@jiayunxie/aerial 0.2.6 → 0.2.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 +7 -3
- package/docs/usage.md +4 -2
- package/package.json +2 -2
- package/src/cli/commands/setup.js +37 -5
- package/src/cli/index.js +1 -1
- package/src/cli/probe.js +4 -1
- package/src/cli/select.js +96 -11
- package/src/proxy/effort-routing.js +89 -21
- package/src/proxy/index.js +3 -3
- package/src/proxy/model-catalog.js +15 -6
- package/src/proxy/models.js +10 -1
- package/src/setup/clients.js +3 -6
- package/src/shared/effort.js +70 -1
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ During setup, Aerial reads your Copilot model list, shows the models that work f
|
|
|
53
53
|
aerial service install
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
On Windows, run this command from an Administrator PowerShell or Command Prompt. Aerial registers its background service with Task Scheduler, and Windows requires elevation for that registration step.
|
|
57
|
+
|
|
56
58
|
4. Check everything from one place:
|
|
57
59
|
|
|
58
60
|
```bash
|
|
@@ -91,10 +93,12 @@ aerial setup claude
|
|
|
91
93
|
To skip the prompts:
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
|
-
aerial setup codex --model <responses-model-id> --effort <low|medium|high|xhigh|max>
|
|
96
|
+
aerial setup codex --model <responses-model-id> --effort <minimal|low|medium|high|xhigh|max|ultra>
|
|
95
97
|
aerial setup claude --model <messages-model-id> --effort <low|medium|high|xhigh|max>
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
Codex effort is model-aware. Aerial preserves an effort when the selected Copilot model advertises it and otherwise selects the nearest usable effort, preferring the highest level below the request. Codex `minimal` maps to Copilot `none`; `none` is also accepted as an input alias. Claude Code keeps its own effort semantics, where `max` is an alias for `xhigh`.
|
|
101
|
+
|
|
98
102
|
To inspect the full model matrix:
|
|
99
103
|
|
|
100
104
|
```bash
|
|
@@ -120,7 +124,7 @@ Advanced override: `AERIAL_UPSTREAM_PROXY` can be set to a bare `http://`, `http
|
|
|
120
124
|
```bash
|
|
121
125
|
aerial status # setup, login, service, and health summary
|
|
122
126
|
aerial proxy status # upstream proxy mode, egress, and route visibility
|
|
123
|
-
aerial service install # install and start the background service
|
|
127
|
+
aerial service install # install and start the background service (Windows: Administrator terminal required)
|
|
124
128
|
aerial doctor # local diagnostics
|
|
125
129
|
aerial teardown # restore client configs and uninstall the service
|
|
126
130
|
```
|
|
@@ -141,7 +145,7 @@ Advanced service lifecycle commands are documented in `docs/usage.md`.
|
|
|
141
145
|
## Notes
|
|
142
146
|
|
|
143
147
|
- macOS background service support uses a user LaunchAgent.
|
|
144
|
-
- Windows background service support uses a user Task Scheduler task.
|
|
148
|
+
- Windows background service support uses a user Task Scheduler task. Run `aerial service install` from an Administrator terminal to register it.
|
|
145
149
|
- Linux service management is not built in yet; run `aerial start` or use your own init system.
|
|
146
150
|
- Copilot inference routes are an observed compatibility target and may change upstream.
|
|
147
151
|
|
package/docs/usage.md
CHANGED
|
@@ -67,6 +67,8 @@ aerial service install
|
|
|
67
67
|
|
|
68
68
|
Default URL: `http://127.0.0.1:18181`. `aerial service install` is the daily-use path on macOS and Windows because it installs and starts the local background service. Use `aerial start` only when you want a foreground debug process in the current terminal.
|
|
69
69
|
|
|
70
|
+
On Windows, run `aerial service install` from an Administrator PowerShell or Command Prompt. Aerial registers the background service with Task Scheduler, and Windows requires elevation for that registration step.
|
|
71
|
+
|
|
70
72
|
## 5. Configure Codex CLI
|
|
71
73
|
|
|
72
74
|
```bash
|
|
@@ -91,7 +93,7 @@ The local key is generated and stored by Aerial automatically. Users do not need
|
|
|
91
93
|
|
|
92
94
|
For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
|
|
93
95
|
|
|
94
|
-
To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <low|medium|high|xhigh|max
|
|
96
|
+
To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <minimal|low|medium|high|xhigh|max|ultra>`. `none` is accepted as an alias for Codex `minimal`. Aerial resolves the request against the selected model's live Copilot capabilities: supported values are preserved, while unsupported values fall back to the nearest usable level (`ultra` currently becomes `max` on GPT-5.6 models, and `minimal` is sent as Copilot `none`). The resolved Codex value is written as root-level `model_reasoning_effort = "<effort>"`. Codex setup does not change Aerial `defaultEffort`, which remains the Claude proxy fallback. Under non-TTY (CI/pipes) the wizard does not prompt and falls back to `medium` when the selected model supports it.
|
|
95
97
|
|
|
96
98
|
## 6. Configure Claude Code
|
|
97
99
|
|
|
@@ -237,7 +239,7 @@ aerial service uninstall
|
|
|
237
239
|
Aerial ships a thin platform wrapper around the user-mode service primitives provided by the host OS — there is no Aerial-specific daemon. Two platforms are supported:
|
|
238
240
|
|
|
239
241
|
- macOS: a user-level launchd `LaunchAgent` at `~/Library/LaunchAgents/com.jiayunxie.aerial.plist` invokes a generated POSIX shell wrapper at `<config-dir>/bin/aerial-service.sh`. The plist is regenerated on every `aerial service install` with a `<!-- Generated by aerial; do not edit -->` header, `KeepAlive = { SuccessfulExit = false; Crashed = true }` so the agent only restarts on crash (not on a clean exit), `ThrottleInterval = 10` to cap restart cadence, and no `StandardOutPath`/`StandardErrorPath` keys (the wrapper owns stdio redirection so launchd does not hold a write fd that would race with rotation). The launchctl command path is `gui/<uid>` (per-user agent, no privilege escalation). Start/stop go through `launchctl bootstrap` and `launchctl bootout` against that domain — never `launchctl kill`.
|
|
240
|
-
- Windows: a Task Scheduler task named `AerialLocalProxy`, `/SC ONLOGON /RL LIMITED`, executes a PowerShell wrapper at `<config-dir>\bin\aerial-service.ps1` (default `%APPDATA%\aerial\bin\aerial-service.ps1`). The wrapper is regenerated on every `aerial service install`. The `/TR` argument quotes the wrapper path with normal double quotes so paths that contain spaces or non-ASCII characters work without manual quoting.
|
|
242
|
+
- Windows: a Task Scheduler task named `AerialLocalProxy`, `/SC ONLOGON /RL LIMITED`, executes a PowerShell wrapper at `<config-dir>\bin\aerial-service.ps1` (default `%APPDATA%\aerial\bin\aerial-service.ps1`). The wrapper is regenerated on every `aerial service install`. Run `aerial service install` from an Administrator PowerShell or Command Prompt so Windows allows Aerial to register or refresh the scheduled task. The task itself still runs at user logon with `/RL LIMITED`. The `/TR` argument quotes the wrapper path with normal double quotes so paths that contain spaces or non-ASCII characters work without manual quoting.
|
|
241
243
|
- Linux: not implemented in this release. `aerial service install|start|stop|restart|uninstall` throws an unsupported-platform error and exits 1; `aerial service status --json` still emits a schema-valid document with `"supported": false` and exits 1. Run `aerial start` directly or wrap it in your own init system.
|
|
242
244
|
|
|
243
245
|
Both wrappers do the same three things before exec-ing the proxy: (1) startup-rotate the captured stdio log (`aerial-stdio.log` → `.1` → `.2` → `.3`) if it has grown beyond the configured cap; (2) export `AERIAL_LOG_FILE=<config-dir>/logs/aerial.log`, plus `AERIAL_LOG_MAX_BYTES` and `AERIAL_LOG_BACKUPS` (default `5242880` / `3`, or whatever value was present in the installer's environment — see below), and — when `AERIAL_CONFIG_DIR` was set at install time — re-export `AERIAL_CONFIG_DIR` so the service sees the same config root as the installer; (3) `exec` `node src/cli/index.js start --host <host> --port <port>` with stdout and stderr appended to `aerial-stdio.log`. By default, the service wrapper uses the same Node.js binary that ran `aerial service install`. Set `AERIAL_SERVICE_NODE` before `aerial service install` to override this explicitly, for example when installing from one Node binary but running the service with another. The structured event log is opt-in via `AERIAL_LOG_FILE`: when this env var is set (always set by the wrapper, never by foreground `aerial start`), structured events go to that file only; when unset, they go to stderr only. There is no double-write.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiayunxie/aerial",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Local GitHub Copilot proxy for Codex CLI and Claude Code with smart HTTP(S)/SOCKS5 upstream proxy support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -47,6 +47,6 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"smol-toml": "^1.6.1",
|
|
49
49
|
"socks": "^2.8.9",
|
|
50
|
-
"undici": "^7.
|
|
50
|
+
"undici": "^7.28.0"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { loadConfig } from "../../shared/config.js";
|
|
2
2
|
import { configPath } from "../../shared/paths.js";
|
|
3
3
|
import { restoreAllClients, restoreClient, setupClaude, setupCodex, setupStatus } from "../../setup/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
assertValidCodexEffort,
|
|
6
|
+
assertValidEffort,
|
|
7
|
+
chooseSetupEffort,
|
|
8
|
+
chooseSetupModel,
|
|
9
|
+
discoverModelsForRoute,
|
|
10
|
+
formatEffortSelection,
|
|
11
|
+
formatModelChoices
|
|
12
|
+
} from "../select.js";
|
|
5
13
|
import { requiredArgValue, claudeApiKeyHelper, codexAuthCommand } from "../helpers.js";
|
|
6
14
|
import { printRestoreResults, printSetupCompletionSummary } from "../output.js";
|
|
7
15
|
|
|
8
16
|
async function selectSetupOptions(target, route, args) {
|
|
9
17
|
const explicitEffort = requiredArgValue(args, "--effort");
|
|
10
|
-
if (explicitEffort !== undefined)
|
|
18
|
+
if (explicitEffort !== undefined) {
|
|
19
|
+
if (target === "Codex") assertValidCodexEffort(explicitEffort);
|
|
20
|
+
else assertValidEffort(explicitEffort);
|
|
21
|
+
}
|
|
11
22
|
const selected = await chooseSetupModel({ target, route, explicitModel: requiredArgValue(args, "--model") });
|
|
12
23
|
if (!selected.displayed) {
|
|
13
24
|
for (const line of formatModelChoices({ target, route, choices: selected.choices, selectedModel: selected.model, source: selected.source, recommended: selected.recommended })) {
|
|
@@ -16,8 +27,26 @@ async function selectSetupOptions(target, route, args) {
|
|
|
16
27
|
} else {
|
|
17
28
|
console.log(`Selected ${target} model: ${selected.model}`);
|
|
18
29
|
}
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
let selectedModel = selected.choices.find((choice) => choice.id === selected.model);
|
|
31
|
+
if (!selectedModel && selected.source === "explicit") {
|
|
32
|
+
try {
|
|
33
|
+
selectedModel = (await discoverModelsForRoute(route)).find((choice) => choice.id === selected.model);
|
|
34
|
+
} catch {
|
|
35
|
+
selectedModel = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const effortChoice = await chooseSetupEffort({
|
|
39
|
+
target,
|
|
40
|
+
explicitEffort,
|
|
41
|
+
model: selected.model,
|
|
42
|
+
supportedEfforts: selectedModel?.supportedEfforts
|
|
43
|
+
});
|
|
44
|
+
console.log(formatEffortSelection({
|
|
45
|
+
target,
|
|
46
|
+
effort: effortChoice.effort,
|
|
47
|
+
source: effortChoice.source,
|
|
48
|
+
supportedEfforts: effortChoice.supportedEfforts
|
|
49
|
+
}));
|
|
21
50
|
return {
|
|
22
51
|
model: selected.model,
|
|
23
52
|
effort: effortChoice.effort,
|
|
@@ -63,7 +92,10 @@ export async function runSetupCli(subcommand, rest) {
|
|
|
63
92
|
aerialDefaultEffort: config.defaultEffort || "missing",
|
|
64
93
|
backup: result.backup,
|
|
65
94
|
auth: "command-backed local Aerial key",
|
|
66
|
-
notes: [
|
|
95
|
+
notes: [
|
|
96
|
+
"Aerial defaultEffort is the Claude proxy fallback and was left unchanged.",
|
|
97
|
+
"restart Codex if it was already running so it reloads the profile."
|
|
98
|
+
]
|
|
67
99
|
});
|
|
68
100
|
return;
|
|
69
101
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ function printHelp() {
|
|
|
18
18
|
Usage:
|
|
19
19
|
aerial --version
|
|
20
20
|
aerial login
|
|
21
|
-
aerial setup codex [--model <id>] [--effort <low|medium|high|xhigh|max>]
|
|
21
|
+
aerial setup codex [--model <id>] [--effort <minimal|low|medium|high|xhigh|max|ultra>]
|
|
22
22
|
aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
|
|
23
23
|
aerial service install
|
|
24
24
|
aerial status [--json]
|
package/src/cli/probe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "../proxy/index.js";
|
|
2
2
|
import { readJsonSafely } from "../shared/utils.js";
|
|
3
3
|
import { aerialRoutes, usageSummary } from "../proxy/models.js";
|
|
4
|
+
import { supportedReasoningEfforts } from "../proxy/model-catalog.js";
|
|
4
5
|
|
|
5
6
|
function modelRoutes(model) {
|
|
6
7
|
return aerialRoutes(model);
|
|
@@ -59,6 +60,7 @@ export async function runProbe({ live = false } = {}) {
|
|
|
59
60
|
models: models.map((model) => ({
|
|
60
61
|
id: model.id,
|
|
61
62
|
routes: modelRoutes(model),
|
|
63
|
+
efforts: supportedReasoningEfforts(model),
|
|
62
64
|
notes: model.aerial?.notes || [],
|
|
63
65
|
supported: Boolean(model.aerial?.supported)
|
|
64
66
|
}))
|
|
@@ -117,8 +119,9 @@ export function formatProbeReport(report) {
|
|
|
117
119
|
lines.push("", "Model matrix:");
|
|
118
120
|
for (const model of report.models) {
|
|
119
121
|
const routes = model.routes.length ? model.routes.join(",") : "-";
|
|
122
|
+
const efforts = model.efforts.length ? ` efforts=${model.efforts.join(",")}` : "";
|
|
120
123
|
const notes = model.notes.length ? ` notes=${model.notes.join(",")}` : "";
|
|
121
|
-
lines.push(`- ${model.id}: routes=${routes}${notes}`);
|
|
124
|
+
lines.push(`- ${model.id}: routes=${routes}${efforts}${notes}`);
|
|
122
125
|
}
|
|
123
126
|
return lines.join("\n");
|
|
124
127
|
}
|
package/src/cli/select.js
CHANGED
|
@@ -4,14 +4,34 @@ import { proxyModels } from "../proxy/index.js";
|
|
|
4
4
|
import { readJsonSafely } from "../shared/utils.js";
|
|
5
5
|
import { modelsForRoute } from "../proxy/models.js";
|
|
6
6
|
import { codexStatus, claudeStatus } from "../setup/clients.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CODEX_EFFORT_VALUES,
|
|
9
|
+
DEFAULT_EFFORT,
|
|
10
|
+
EFFORT_VALUES,
|
|
11
|
+
assertValidCodexEffort,
|
|
12
|
+
assertValidEffort,
|
|
13
|
+
normalizeCodexEffort,
|
|
14
|
+
normalizeEffort,
|
|
15
|
+
resolveCodexEffort
|
|
16
|
+
} from "../shared/effort.js";
|
|
8
17
|
|
|
9
18
|
export { modelsForRoute } from "../proxy/models.js";
|
|
10
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
CODEX_EFFORT_VALUES,
|
|
21
|
+
DEFAULT_EFFORT,
|
|
22
|
+
EFFORT_VALUES,
|
|
23
|
+
assertValidCodexEffort,
|
|
24
|
+
assertValidEffort,
|
|
25
|
+
normalizeCodexEffort,
|
|
26
|
+
normalizeEffort,
|
|
27
|
+
resolveCodexEffort
|
|
28
|
+
} from "../shared/effort.js";
|
|
11
29
|
|
|
12
30
|
const MAX_LISTED_MODELS = 20;
|
|
13
31
|
const GPT_VERSION_RE = /^gpt-(\d+)(?:\.(\d+))?/i;
|
|
14
32
|
const STABLE_GPT_RE = /^gpt-\d+(?:\.\d+)?$/i;
|
|
33
|
+
const CODEX_EFFORT_USAGE = "<minimal|low|medium|high|xhigh|max|ultra>";
|
|
34
|
+
const CLAUDE_EFFORT_USAGE = "<low|medium|high|xhigh|max>";
|
|
15
35
|
|
|
16
36
|
const CLEAR_LINE = "\x1b[2K";
|
|
17
37
|
const CURSOR_UP = "\x1b[1A";
|
|
@@ -219,12 +239,45 @@ function currentModelFor(target) {
|
|
|
219
239
|
function currentEffortFor(target) {
|
|
220
240
|
try {
|
|
221
241
|
const status = target === "Codex" ? codexStatus() : claudeStatus();
|
|
222
|
-
|
|
242
|
+
if (typeof status.effort !== "string") return undefined;
|
|
243
|
+
return target === "Codex" ? normalizeCodexEffort(status.effort) : normalizeEffort(status.effort);
|
|
223
244
|
} catch {
|
|
224
245
|
return undefined;
|
|
225
246
|
}
|
|
226
247
|
}
|
|
227
248
|
|
|
249
|
+
export function normalizeEffortCandidates(values) {
|
|
250
|
+
if (!Array.isArray(values)) return [];
|
|
251
|
+
const normalized = new Set();
|
|
252
|
+
for (const value of values) {
|
|
253
|
+
const effort = normalizeEffort(value);
|
|
254
|
+
if (effort) normalized.add(effort);
|
|
255
|
+
}
|
|
256
|
+
return EFFORT_VALUES.filter((effort) => normalized.has(effort));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function normalizeCodexEffortCandidates(values) {
|
|
260
|
+
if (!Array.isArray(values)) return [];
|
|
261
|
+
const normalized = new Set();
|
|
262
|
+
for (const value of values) {
|
|
263
|
+
const effort = normalizeCodexEffort(value);
|
|
264
|
+
if (effort) normalized.add(effort);
|
|
265
|
+
}
|
|
266
|
+
return CODEX_EFFORT_VALUES.filter((effort) => normalized.has(effort));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function effortUsage(target, candidates, restricted) {
|
|
270
|
+
if (!restricted) return target === "Codex" ? CODEX_EFFORT_USAGE : CLAUDE_EFFORT_USAGE;
|
|
271
|
+
if (target === "Codex") return `<${candidates.join("|")}>`;
|
|
272
|
+
const values = [...candidates];
|
|
273
|
+
if (values.includes("xhigh")) values.push("max");
|
|
274
|
+
return `<${values.join("|")}>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function defaultEffortFor(candidates) {
|
|
278
|
+
return candidates.includes(DEFAULT_EFFORT) ? DEFAULT_EFFORT : candidates[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
228
281
|
export async function chooseSetupModel({
|
|
229
282
|
target,
|
|
230
283
|
route,
|
|
@@ -302,23 +355,48 @@ export function formatModelChoices({ target, route, choices, selectedModel, sour
|
|
|
302
355
|
export async function chooseSetupEffort({
|
|
303
356
|
target,
|
|
304
357
|
explicitEffort,
|
|
358
|
+
model,
|
|
359
|
+
supportedEfforts,
|
|
305
360
|
prompt,
|
|
306
361
|
input: inputStream = input,
|
|
307
362
|
output: outputStream = output
|
|
308
363
|
} = {}) {
|
|
364
|
+
const isCodex = target === "Codex";
|
|
365
|
+
const restrictedEfforts = isCodex
|
|
366
|
+
? normalizeCodexEffortCandidates(supportedEfforts)
|
|
367
|
+
: normalizeEffortCandidates(supportedEfforts);
|
|
368
|
+
const restricted = restrictedEfforts.length > 0;
|
|
369
|
+
const globalEfforts = isCodex ? CODEX_EFFORT_VALUES : EFFORT_VALUES;
|
|
370
|
+
const candidates = restricted ? restrictedEfforts : globalEfforts;
|
|
309
371
|
if (explicitEffort !== undefined) {
|
|
310
|
-
|
|
372
|
+
if (isCodex) {
|
|
373
|
+
const requested = assertValidCodexEffort(explicitEffort);
|
|
374
|
+
const resolved = resolveCodexEffort(requested, supportedEfforts);
|
|
375
|
+
return {
|
|
376
|
+
effort: resolved.resolvedEffort,
|
|
377
|
+
source: "explicit",
|
|
378
|
+
displayed: false,
|
|
379
|
+
supportedEfforts: restricted ? candidates : undefined
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const effort = assertValidEffort(explicitEffort);
|
|
383
|
+
if (restricted && !candidates.includes(effort)) {
|
|
384
|
+
const subject = model ? `${target} model ${model}` : `${target} model`;
|
|
385
|
+
throw new Error(`Effort ${JSON.stringify(explicitEffort)} is not supported by ${subject}. Allowed: ${effortUsage(target, candidates, true)}.`);
|
|
386
|
+
}
|
|
387
|
+
return { effort, source: "explicit", displayed: false, supportedEfforts: restricted ? candidates : undefined };
|
|
311
388
|
}
|
|
312
389
|
const shouldPrompt = prompt === undefined ? Boolean(inputStream.isTTY) : prompt;
|
|
313
390
|
if (!shouldPrompt) {
|
|
314
|
-
return { effort:
|
|
391
|
+
return { effort: defaultEffortFor(candidates), source: "default_non_tty", displayed: false, supportedEfforts: restricted ? candidates : undefined };
|
|
315
392
|
}
|
|
316
393
|
const current = currentEffortFor(target);
|
|
317
|
-
const currentIndex =
|
|
318
|
-
const
|
|
394
|
+
const currentIndex = candidates.indexOf(current);
|
|
395
|
+
const defaultIndex = candidates.indexOf(DEFAULT_EFFORT);
|
|
396
|
+
const initialIndex = currentIndex >= 0 ? currentIndex : defaultIndex >= 0 ? defaultIndex : 0;
|
|
319
397
|
const { item, cancelled } = await select({
|
|
320
398
|
title: `Choose ${target} reasoning effort:`,
|
|
321
|
-
items:
|
|
399
|
+
items: candidates.map((value) => ({ label: value, value })),
|
|
322
400
|
initialIndex,
|
|
323
401
|
getTags: (it) => {
|
|
324
402
|
const tags = [];
|
|
@@ -330,11 +408,18 @@ export async function chooseSetupEffort({
|
|
|
330
408
|
output: outputStream
|
|
331
409
|
});
|
|
332
410
|
if (cancelled) throw new Error(`${target} setup cancelled.`);
|
|
333
|
-
|
|
411
|
+
const effort = isCodex
|
|
412
|
+
? resolveCodexEffort(item.value, supportedEfforts).resolvedEffort
|
|
413
|
+
: item.value;
|
|
414
|
+
return { effort, source: "prompt", displayed: true, supportedEfforts: restricted ? candidates : undefined };
|
|
334
415
|
}
|
|
335
416
|
|
|
336
|
-
export function formatEffortSelection({ target, effort, source }) {
|
|
417
|
+
export function formatEffortSelection({ target, effort, source, supportedEfforts }) {
|
|
337
418
|
if (source === "explicit") return `Selected ${target} effort: ${effort}`;
|
|
338
419
|
if (source === "prompt") return `Selected ${target} effort: ${effort}`;
|
|
339
|
-
|
|
420
|
+
const restrictedEfforts = target === "Codex"
|
|
421
|
+
? normalizeCodexEffortCandidates(supportedEfforts)
|
|
422
|
+
: normalizeEffortCandidates(supportedEfforts);
|
|
423
|
+
const restricted = restrictedEfforts.length > 0;
|
|
424
|
+
return `No interactive terminal detected; selected ${target} effort: ${effort}. Pass --effort ${effortUsage(target, restrictedEfforts, restricted)} to choose a different effort.`;
|
|
340
425
|
}
|
|
@@ -1,44 +1,97 @@
|
|
|
1
1
|
import { loadConfig } from "../shared/config.js";
|
|
2
2
|
import { logEvent } from "../shared/log.js";
|
|
3
|
-
import { EFFORT_VALUES, normalizeEffort } from "../shared/effort.js";
|
|
4
|
-
import {
|
|
3
|
+
import { EFFORT_VALUES, normalizeCodexEffort, normalizeEffort, resolveCodexEffort } from "../shared/effort.js";
|
|
4
|
+
import {
|
|
5
|
+
canonicalClaudeFamily,
|
|
6
|
+
findCompatibleModel as findCompatibleModelShared,
|
|
7
|
+
supportedReasoningEfforts
|
|
8
|
+
} from "./model-catalog.js";
|
|
5
9
|
import { withDefaultAnthropicCache, withDefaultPromptCache } from "./cache-policy.js";
|
|
6
10
|
|
|
7
|
-
function openAIEffortRoute(model, effort) {
|
|
11
|
+
function openAIEffortRoute(model, effort, supportedEfforts) {
|
|
8
12
|
if (effort === undefined) return undefined;
|
|
9
|
-
const normalized =
|
|
13
|
+
const normalized = normalizeCodexEffort(effort);
|
|
10
14
|
if (!normalized) return undefined;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return undefined;
|
|
15
|
+
const requested = String(effort).trim().toLowerCase();
|
|
16
|
+
const resolved = resolveCodexEffort(normalized, supportedEfforts);
|
|
17
|
+
if (!resolved) return undefined;
|
|
18
|
+
const hasSupportedEffortMetadata = Array.isArray(supportedEfforts)
|
|
19
|
+
&& supportedEfforts.some((value) => normalizeCodexEffort(value));
|
|
20
|
+
if (!hasSupportedEffortMetadata && /^gpt-5-mini(?:-|$)/.test(model) && resolved.wireEffort === "xhigh") {
|
|
21
|
+
return { effort: "high", reason: "model_compatibility" };
|
|
22
|
+
}
|
|
23
|
+
if (resolved.wireEffort === requested) return undefined;
|
|
24
|
+
return { effort: resolved.wireEffort, reason: resolved.reason };
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
function withSupportedOpenAIEffort(payload) {
|
|
27
|
+
async function withSupportedOpenAIEffort(payload, loadModels) {
|
|
17
28
|
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
18
29
|
const reasoningEffort = payload?.reasoning && typeof payload.reasoning === "object" ? payload.reasoning.effort : undefined;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
30
|
+
const flatEffort = payload?.reasoning_effort;
|
|
31
|
+
const hasKnownEffort = normalizeCodexEffort(reasoningEffort) || normalizeCodexEffort(flatEffort);
|
|
32
|
+
const models = hasKnownEffort && typeof loadModels === "function"
|
|
33
|
+
? await loadModels().catch(() => undefined)
|
|
34
|
+
: undefined;
|
|
35
|
+
const selectedModel = Array.isArray(models) ? models.find((entry) => entry?.id === model) : undefined;
|
|
36
|
+
const supportedEfforts = selectedModel ? supportedReasoningEfforts(selectedModel) : undefined;
|
|
37
|
+
const nextReasoningEffort = openAIEffortRoute(model, reasoningEffort, supportedEfforts);
|
|
38
|
+
const nextFlatEffort = openAIEffortRoute(model, flatEffort, supportedEfforts);
|
|
21
39
|
if (!nextReasoningEffort && !nextFlatEffort) return payload;
|
|
22
40
|
|
|
23
41
|
const next = { ...payload };
|
|
24
|
-
if (nextReasoningEffort) next.reasoning = { ...payload.reasoning, effort: nextReasoningEffort };
|
|
25
|
-
if (nextFlatEffort) next.reasoning_effort = nextFlatEffort;
|
|
42
|
+
if (nextReasoningEffort) next.reasoning = { ...payload.reasoning, effort: nextReasoningEffort.effort };
|
|
43
|
+
if (nextFlatEffort) next.reasoning_effort = nextFlatEffort.effort;
|
|
26
44
|
logEvent("openai_effort_route", {
|
|
27
45
|
model,
|
|
28
|
-
effort: reasoningEffort ??
|
|
29
|
-
routedEffort: nextReasoningEffort ?? nextFlatEffort
|
|
46
|
+
effort: reasoningEffort ?? flatEffort,
|
|
47
|
+
routedEffort: nextReasoningEffort?.effort ?? nextFlatEffort?.effort,
|
|
48
|
+
reason: nextReasoningEffort?.reason ?? nextFlatEffort?.reason
|
|
30
49
|
});
|
|
31
50
|
return next;
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
export function withOpenAIDefaults(payload) {
|
|
35
|
-
return withDefaultPromptCache(withSupportedOpenAIEffort(payload));
|
|
53
|
+
export async function withOpenAIDefaults(payload, loadModels) {
|
|
54
|
+
return withDefaultPromptCache(await withSupportedOpenAIEffort(payload, loadModels));
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
function objectOrEmpty(value) {
|
|
39
58
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
40
59
|
}
|
|
41
60
|
|
|
61
|
+
function closestSupportedEffort(model, requestedEffort) {
|
|
62
|
+
const requestedRank = EFFORT_VALUES.indexOf(requestedEffort);
|
|
63
|
+
if (requestedRank === -1) return undefined;
|
|
64
|
+
const supported = supportedReasoningEfforts(model)
|
|
65
|
+
.map((effort) => normalizeEffort(effort))
|
|
66
|
+
.filter((effort, index, values) => effort && values.indexOf(effort) === index);
|
|
67
|
+
if (supported.includes(requestedEffort)) return requestedEffort;
|
|
68
|
+
if (supported.length === 0) return undefined;
|
|
69
|
+
const ranked = supported
|
|
70
|
+
.map((effort) => ({ effort, rank: EFFORT_VALUES.indexOf(effort) }))
|
|
71
|
+
.filter((entry) => entry.rank !== -1)
|
|
72
|
+
.sort((a, b) => a.rank - b.rank);
|
|
73
|
+
const lowerOrEqual = ranked.filter((entry) => entry.rank <= requestedRank);
|
|
74
|
+
return (lowerOrEqual.at(-1) || ranked[0])?.effort;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function preferredModel(models, modelId) {
|
|
78
|
+
if (!Array.isArray(models)) return undefined;
|
|
79
|
+
return models.find((model) => model?.id === modelId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fallbackModelForFamily(models, family, preferredId) {
|
|
83
|
+
const exact = preferredModel(models, preferredId);
|
|
84
|
+
if (exact) return exact;
|
|
85
|
+
const base = preferredModel(models, family);
|
|
86
|
+
if (!base) return undefined;
|
|
87
|
+
return findCompatibleModelShared({
|
|
88
|
+
models: [base],
|
|
89
|
+
family,
|
|
90
|
+
route: "/v1/messages",
|
|
91
|
+
adaptiveThinking: true
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
42
95
|
function withSupportedAnthropicEffort(payload, models) {
|
|
43
96
|
const effort = payload?.output_config?.effort;
|
|
44
97
|
if (effort === undefined) return payload;
|
|
@@ -47,18 +100,33 @@ function withSupportedAnthropicEffort(payload, models) {
|
|
|
47
100
|
if (!family) return payload;
|
|
48
101
|
const nextEffort = normalizeEffort(effort);
|
|
49
102
|
if (!nextEffort || !EFFORT_VALUES.includes(nextEffort)) return payload;
|
|
50
|
-
|
|
103
|
+
let routedEffort = nextEffort;
|
|
104
|
+
let routed = findCompatibleModelShared({
|
|
51
105
|
models,
|
|
52
106
|
family,
|
|
53
107
|
route: "/v1/messages",
|
|
54
108
|
adaptiveThinking: true,
|
|
55
|
-
effort:
|
|
109
|
+
effort: routedEffort,
|
|
56
110
|
preferredId: model
|
|
57
111
|
});
|
|
112
|
+
if (!routed) {
|
|
113
|
+
const fallbackEffort = closestSupportedEffort(fallbackModelForFamily(models, family, model), nextEffort);
|
|
114
|
+
if (fallbackEffort && fallbackEffort !== nextEffort) {
|
|
115
|
+
routedEffort = fallbackEffort;
|
|
116
|
+
routed = findCompatibleModelShared({
|
|
117
|
+
models,
|
|
118
|
+
family,
|
|
119
|
+
route: "/v1/messages",
|
|
120
|
+
adaptiveThinking: true,
|
|
121
|
+
effort: routedEffort,
|
|
122
|
+
preferredId: model
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
58
126
|
const nextModel = routed?.id || model;
|
|
59
|
-
if (model === nextModel && effort ===
|
|
60
|
-
logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort
|
|
61
|
-
return { ...payload, model: nextModel, output_config: { ...payload.output_config, effort:
|
|
127
|
+
if (model === nextModel && effort === routedEffort) return payload;
|
|
128
|
+
logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort });
|
|
129
|
+
return { ...payload, model: nextModel, output_config: { ...payload.output_config, effort: routedEffort } };
|
|
62
130
|
}
|
|
63
131
|
|
|
64
132
|
function legacyThinkingEffort(thinking) {
|
package/src/proxy/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function proxyModels(request) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function proxyResponses(request) {
|
|
27
|
-
const payload = withOpenAIDefaults(await request.json());
|
|
27
|
+
const payload = await withOpenAIDefaults(await request.json(), fetchModelsCatalogForCopilot);
|
|
28
28
|
const body = Buffer.from(JSON.stringify(payload));
|
|
29
29
|
|
|
30
30
|
if (payload?.stream && isResponsesWebSocketOptIn()) {
|
|
@@ -67,8 +67,8 @@ export async function proxyMessages(request) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export async function proxyChatCompletions(request) {
|
|
70
|
-
const upstreamRequest = await requestWithJsonBody(request, (payload) => {
|
|
71
|
-
payload = withOpenAIDefaults(payload);
|
|
70
|
+
const upstreamRequest = await requestWithJsonBody(request, async (payload) => {
|
|
71
|
+
payload = await withOpenAIDefaults(payload, fetchModelsCatalogForCopilot);
|
|
72
72
|
if (payload.max_tokens !== undefined && payload.max_completion_tokens === undefined) {
|
|
73
73
|
const { max_tokens, ...rest } = payload;
|
|
74
74
|
return { ...rest, max_completion_tokens: max_tokens };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { logEvent } from "../shared/log.js";
|
|
2
3
|
|
|
3
4
|
const TTL_MS = 30000;
|
|
4
5
|
const cache = new Map();
|
|
@@ -8,14 +9,21 @@ export function tokenFingerprintOf(token) {
|
|
|
8
9
|
return crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export async function fetchModelsCatalog({ fetchImpl, tokenFingerprint } = {}) {
|
|
12
|
+
export async function fetchModelsCatalog({ fetchImpl, tokenFingerprint, now = Date.now() } = {}) {
|
|
12
13
|
if (typeof fetchImpl !== "function") return undefined;
|
|
13
14
|
const key = tokenFingerprint || "anonymous";
|
|
14
15
|
const cached = cache.get(key);
|
|
15
|
-
const now = Date.now();
|
|
16
16
|
if (cached && cached.expiresAt > now) return cached.models;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let models;
|
|
18
|
+
try {
|
|
19
|
+
models = await fetchImpl();
|
|
20
|
+
} catch {
|
|
21
|
+
models = undefined;
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(models)) {
|
|
24
|
+
if (cached) logEvent("model_catalog_stale", { reason: "refresh_failed" });
|
|
25
|
+
return cached?.models;
|
|
26
|
+
}
|
|
19
27
|
cache.set(key, { models, expiresAt: now + TTL_MS });
|
|
20
28
|
return models;
|
|
21
29
|
}
|
|
@@ -26,7 +34,8 @@ export function clearModelCatalogCacheForTests() {
|
|
|
26
34
|
|
|
27
35
|
export function canonicalClaudeFamily(modelId) {
|
|
28
36
|
if (typeof modelId !== "string") return undefined;
|
|
29
|
-
|
|
37
|
+
const opus = modelId.match(/^claude-opus-4[.-](\d+)(?:-|$)/);
|
|
38
|
+
if (opus) return `claude-opus-4.${opus[1]}`;
|
|
30
39
|
return undefined;
|
|
31
40
|
}
|
|
32
41
|
|
|
@@ -42,7 +51,7 @@ function modelSupportsAdaptiveThinking(model) {
|
|
|
42
51
|
return supports?.adaptive_thinking === true || supports?.thinking?.adaptive === true;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
function supportedReasoningEfforts(model) {
|
|
54
|
+
export function supportedReasoningEfforts(model) {
|
|
46
55
|
const supports = model?.capabilities?.supports;
|
|
47
56
|
const values = supports?.reasoning_effort ?? supports?.reasoning_efforts;
|
|
48
57
|
if (Array.isArray(values)) return values.map(String);
|
package/src/proxy/models.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { copyResponseHeaders } from "./headers.js";
|
|
2
|
+
import { supportedReasoningEfforts } from "./model-catalog.js";
|
|
2
3
|
|
|
3
4
|
export function aerialSupportForModel(model) {
|
|
4
5
|
const endpoints = Array.isArray(model.supported_endpoints) ? model.supported_endpoints : [];
|
|
@@ -35,7 +36,15 @@ export function aerialRoutes(model) {
|
|
|
35
36
|
export function modelsForRoute(models, route) {
|
|
36
37
|
return models
|
|
37
38
|
.filter((model) => typeof model?.id === "string" && aerialRoutes(model).includes(route))
|
|
38
|
-
.map((model) =>
|
|
39
|
+
.map((model) => {
|
|
40
|
+
const supportedEfforts = supportedReasoningEfforts(model);
|
|
41
|
+
return {
|
|
42
|
+
id: model.id,
|
|
43
|
+
routes: aerialRoutes(model),
|
|
44
|
+
notes: model.aerial?.notes || [],
|
|
45
|
+
...(supportedEfforts.length ? { supportedEfforts } : {})
|
|
46
|
+
};
|
|
47
|
+
});
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
export function usageSummary(payload) {
|
package/src/setup/clients.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parse as parseToml } from "smol-toml";
|
|
|
5
5
|
import { ensureApiKey, loadConfig, saveConfig } from "../shared/config.js";
|
|
6
6
|
import { logEvent } from "../shared/log.js";
|
|
7
7
|
import { atomicWriteFile } from "../shared/utils.js";
|
|
8
|
-
import { assertValidEffort, normalizeEffort } from "../shared/effort.js";
|
|
8
|
+
import { assertValidCodexEffort, assertValidEffort, normalizeCodexEffort, normalizeEffort } from "../shared/effort.js";
|
|
9
9
|
import { backupIfExists, backupPathsFor } from "./restore.js";
|
|
10
10
|
import { setTomlRootString, upsertTomlSection, removeTomlSection } from "./toml.js";
|
|
11
11
|
|
|
@@ -38,7 +38,7 @@ function claudeEnvForAerial(currentEnv, config) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function setupCodex({ model, effort, authCommand = DEFAULT_CODEX_AUTH } = {}) {
|
|
41
|
-
const normalizedEffort = effort === undefined ? undefined :
|
|
41
|
+
const normalizedEffort = effort === undefined ? undefined : assertValidCodexEffort(effort);
|
|
42
42
|
ensureApiKey();
|
|
43
43
|
const config = loadConfig();
|
|
44
44
|
const selectedModel = model || config.defaultModel;
|
|
@@ -65,9 +65,6 @@ export function setupCodex({ model, effort, authCommand = DEFAULT_CODEX_AUTH } =
|
|
|
65
65
|
});
|
|
66
66
|
content = removeTomlSection(content, "profiles.aerial");
|
|
67
67
|
atomicWriteFile(file, content);
|
|
68
|
-
if (normalizedEffort && config.defaultEffort !== normalizedEffort) {
|
|
69
|
-
saveConfig({ ...config, defaultEffort: normalizedEffort });
|
|
70
|
-
}
|
|
71
68
|
logEvent("setup_write", { target: "codex", file, backup, auth: "command", effort: normalizedEffort });
|
|
72
69
|
return { file, backup, model: selectedModel, effort: normalizedEffort, auth: { type: "command", command: authCommand.command, args: authCommand.args || [] } };
|
|
73
70
|
}
|
|
@@ -146,7 +143,7 @@ export function codexStatus() {
|
|
|
146
143
|
? doc.model_providers.aerial.base_url
|
|
147
144
|
: undefined;
|
|
148
145
|
const rootEffort = doc?.model_reasoning_effort;
|
|
149
|
-
const effort = typeof rootEffort === "string" ? (
|
|
146
|
+
const effort = typeof rootEffort === "string" ? (normalizeCodexEffort(rootEffort) || "missing") : "missing";
|
|
150
147
|
const legacyProfile = doc?.profiles?.aerial && typeof doc.profiles.aerial === "object";
|
|
151
148
|
const legacyProfileLooksAerial = legacyProfile
|
|
152
149
|
&& (doc.profiles.aerial.model_provider === "aerial"
|
package/src/shared/effort.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
export const EFFORT_VALUES = Object.freeze(["low", "medium", "high", "xhigh"]);
|
|
2
|
+
export const CODEX_EFFORT_VALUES = Object.freeze(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
2
3
|
export const DEFAULT_EFFORT = "medium";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
function lower(value) {
|
|
5
6
|
if (value === undefined || value === null) return undefined;
|
|
6
7
|
const trimmed = String(value).trim().toLowerCase();
|
|
8
|
+
return trimmed || undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeEffort(value) {
|
|
12
|
+
const trimmed = lower(value);
|
|
7
13
|
if (!trimmed) return undefined;
|
|
8
14
|
if (trimmed === "max") return "xhigh";
|
|
9
15
|
if (EFFORT_VALUES.includes(trimmed)) return trimmed;
|
|
10
16
|
return undefined;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
export function normalizeCodexEffort(value) {
|
|
20
|
+
const trimmed = lower(value);
|
|
21
|
+
if (!trimmed) return undefined;
|
|
22
|
+
if (trimmed === "none") return "minimal";
|
|
23
|
+
if (CODEX_EFFORT_VALUES.includes(trimmed)) return trimmed;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
export function assertValidEffort(raw) {
|
|
14
28
|
const normalized = normalizeEffort(raw);
|
|
15
29
|
if (!normalized) {
|
|
@@ -17,3 +31,58 @@ export function assertValidEffort(raw) {
|
|
|
17
31
|
}
|
|
18
32
|
return normalized;
|
|
19
33
|
}
|
|
34
|
+
|
|
35
|
+
export function assertValidCodexEffort(raw) {
|
|
36
|
+
const normalized = normalizeCodexEffort(raw);
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
throw new Error(`Invalid --effort ${JSON.stringify(raw)} for Codex. Allowed: ${CODEX_EFFORT_VALUES.join(", ")} (or alias 'none' for minimal).`);
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function codexCatalogEfforts(values) {
|
|
44
|
+
if (!Array.isArray(values)) return [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const efforts = [];
|
|
47
|
+
for (const value of values) {
|
|
48
|
+
const wireEffort = lower(value);
|
|
49
|
+
const resolvedEffort = normalizeCodexEffort(wireEffort);
|
|
50
|
+
if (!resolvedEffort || seen.has(resolvedEffort)) continue;
|
|
51
|
+
seen.add(resolvedEffort);
|
|
52
|
+
efforts.push({ resolvedEffort, wireEffort, rank: CODEX_EFFORT_VALUES.indexOf(resolvedEffort) });
|
|
53
|
+
}
|
|
54
|
+
return efforts.sort((a, b) => a.rank - b.rank);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveCodexEffort(requested, supportedEfforts) {
|
|
58
|
+
const requestedEffort = normalizeCodexEffort(requested);
|
|
59
|
+
if (!requestedEffort) return undefined;
|
|
60
|
+
const requestedRank = CODEX_EFFORT_VALUES.indexOf(requestedEffort);
|
|
61
|
+
const supported = codexCatalogEfforts(supportedEfforts);
|
|
62
|
+
|
|
63
|
+
if (supported.length) {
|
|
64
|
+
const exact = supported.find((entry) => entry.resolvedEffort === requestedEffort);
|
|
65
|
+
const lowerOrEqual = supported.filter((entry) => entry.rank <= requestedRank);
|
|
66
|
+
const selected = exact || lowerOrEqual.at(-1) || supported[0];
|
|
67
|
+
let reason = "exact";
|
|
68
|
+
if (selected.resolvedEffort !== requestedEffort) {
|
|
69
|
+
reason = selected.rank < requestedRank ? "downgraded" : "nearest_supported";
|
|
70
|
+
} else if (selected.wireEffort !== requestedEffort) {
|
|
71
|
+
reason = "alias";
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
requestedEffort,
|
|
75
|
+
resolvedEffort: selected.resolvedEffort,
|
|
76
|
+
wireEffort: selected.wireEffort,
|
|
77
|
+
reason
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (requestedEffort === "minimal") {
|
|
82
|
+
return { requestedEffort, resolvedEffort: "minimal", wireEffort: "none", reason: "alias" };
|
|
83
|
+
}
|
|
84
|
+
if (requestedEffort === "ultra") {
|
|
85
|
+
return { requestedEffort, resolvedEffort: "max", wireEffort: "max", reason: "fallback" };
|
|
86
|
+
}
|
|
87
|
+
return { requestedEffort, resolvedEffort: requestedEffort, wireEffort: requestedEffort, reason: "exact" };
|
|
88
|
+
}
|