@jiayunxie/aerial 0.2.3 → 0.2.5

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/docs/usage.md CHANGED
@@ -105,7 +105,7 @@ If Claude Code was previously pointed at another Anthropic-compatible gateway, s
105
105
 
106
106
  For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
107
107
 
108
- 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:
108
+ To skip the prompts, pass `--model <messages-model-id>` and/or `--effort <low|medium|high|xhigh|max>`. `setup claude --effort <value>` writes Claude Code's native `"effortLevel": "<effort>"` setting into `settings.json` and also updates Aerial-wide `defaultEffort` as a proxy fallback. Precedence for the Anthropic effort applied to a Claude Opus 4.7 request, in order:
109
109
 
110
110
  1. An explicit `output_config.effort` in the request body is preserved verbatim.
111
111
  2. A legacy `thinking: { type: "enabled", budget_tokens }` is translated to `thinking: { type: "adaptive" }` with a derived `output_config.effort`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiayunxie/aerial",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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,
@@ -1,7 +1,7 @@
1
- import { defaultConfig, loadConfig, saveConfig } from "../shared/config.js";
2
- import { configPath } from "../shared/paths.js";
3
- import { assertValidEffort } from "../shared/effort.js";
4
- import { parseConfigHost, parseConfigPort } from "./args.js";
1
+ import { defaultConfig, loadConfig, saveConfig } from "../../shared/config.js";
2
+ import { configPath } from "../../shared/paths.js";
3
+ import { assertValidEffort } from "../../shared/effort.js";
4
+ import { parseConfigHost, parseConfigPort } from "../args.js";
5
5
 
6
6
  export function runConfigCli(subcommand, rest) {
7
7
  if (subcommand === "reset") {
@@ -1,7 +1,7 @@
1
- import { restoreAllClients } from "../setup/index.js";
2
- import { serviceUninstall } from "../service/index.js";
3
- import { printRestoreResults } from "./output.js";
4
- import { printServiceUninstallResult } from "./service-command.js";
1
+ import { restoreAllClients } from "../../setup/index.js";
2
+ import { serviceUninstall } from "../../service/index.js";
3
+ import { printRestoreResults } from "../output.js";
4
+ import { printServiceUninstallResult } from "./service.js";
5
5
 
6
6
  export function runDisableCli() {
7
7
  const { ok: restoreOk, results } = restoreAllClients();
@@ -1,4 +1,4 @@
1
- import { ensureApiKey } from "../shared/config.js";
1
+ import { ensureApiKey } from "../../shared/config.js";
2
2
 
3
3
  export function runKeyCli(subcommand) {
4
4
  if (subcommand === "generate") {
@@ -1,4 +1,4 @@
1
- import { gitHubTokenSource, pollDeviceFlow, startDeviceFlow } from "../shared/auth.js";
1
+ import { gitHubTokenSource, pollDeviceFlow, startDeviceFlow } from "../../shared/auth.js";
2
2
 
3
3
  export async function runLoginCli(args) {
4
4
  const force = args.includes("--force");
@@ -1,8 +1,8 @@
1
- import { loadConfig } from "../shared/config.js";
2
- import { disableUpstreamProxy, enableUpstreamProxy, probeEgress, upstreamProxyState } from "../upstream/fetch.js";
3
- import { redactProxyEndpoint, redactProxySource } from "../upstream/proxy-config.js";
4
- import { runProbe } from "./probe.js";
5
- import { formatEgress } from "./output.js";
1
+ import { loadConfig } from "../../shared/config.js";
2
+ import { disableUpstreamProxy, enableUpstreamProxy, probeEgress, upstreamProxyState } from "../../upstream/fetch.js";
3
+ import { redactProxyEndpoint, redactProxySource } from "../../upstream/proxy-config.js";
4
+ import { runProbe } from "../probe.js";
5
+ import { formatEgress } from "../output.js";
6
6
 
7
7
  async function proxyRouteSummary() {
8
8
  try {
@@ -1,5 +1,5 @@
1
- import { serviceInstall, serviceRestart, serviceStart, serviceStatus, serviceStop, serviceUninstall } from "../service/index.js";
2
- import { printServiceDiagnostics, printServiceWarning } from "./output.js";
1
+ import { serviceInstall, serviceRestart, serviceStart, serviceStatus, serviceStop, serviceUninstall } from "../../service/index.js";
2
+ import { printServiceDiagnostics, printServiceWarning } from "../output.js";
3
3
 
4
4
  export function printServiceUninstallResult(r, { prefix = "Service uninstall" } = {}) {
5
5
  if (r.note === "no service installed") console.log(`${prefix}: no service installed`);
@@ -1,11 +1,11 @@
1
- import { loadConfig } from "../shared/config.js";
2
- import { configPath } from "../shared/paths.js";
3
- import { restoreAllClients, restoreClient, setupClaude, setupCodex, setupStatus } from "../setup/index.js";
4
- import { chooseSetupModel, formatModelChoices } from "./model-selection.js";
5
- import { assertValidEffort, chooseSetupEffort, formatEffortSelection } from "./setup-selection.js";
6
- import { requiredArgValue } from "./args.js";
7
- import { claudeApiKeyHelper, codexAuthCommand } from "./runtime-auth.js";
8
- import { printRestoreResults, printSetupCompletionSummary } from "./output.js";
1
+ import { loadConfig } from "../../shared/config.js";
2
+ import { configPath } from "../../shared/paths.js";
3
+ import { restoreAllClients, restoreClient, setupClaude, setupCodex, setupStatus } from "../../setup/index.js";
4
+ import { chooseSetupModel, formatModelChoices } from "../model-selection.js";
5
+ import { assertValidEffort, chooseSetupEffort, formatEffortSelection } from "../setup-selection.js";
6
+ import { requiredArgValue } from "../args.js";
7
+ import { claudeApiKeyHelper, codexAuthCommand } from "../runtime-auth.js";
8
+ import { printRestoreResults, printSetupCompletionSummary } from "../output.js";
9
9
 
10
10
  async function selectSetupOptions(target, route, args) {
11
11
  const explicitEffort = requiredArgValue(args, "--effort");
@@ -83,7 +83,7 @@ export async function runSetupCli(subcommand, rest) {
83
83
  aerialDefaultEffort: config.defaultEffort || "missing",
84
84
  backup: result.backup,
85
85
  auth: "apiKeyHelper local Aerial key",
86
- notes: ["effort is applied via Aerial defaultEffort and proxy fallback; Claude settings.json does not store an effort value."]
86
+ notes: ["effort is written to Claude settings.json effortLevel and mirrored to Aerial defaultEffort for proxy fallback."]
87
87
  });
88
88
  return;
89
89
  }
@@ -1,6 +1,6 @@
1
- import { ensureApiKey, loadConfig } from "../shared/config.js";
2
- import { startServer } from "../proxy/server.js";
3
- import { argValue } from "./args.js";
1
+ import { ensureApiKey, loadConfig } from "../../shared/config.js";
2
+ import { startServer } from "../../proxy/server.js";
3
+ import { argValue } from "../args.js";
4
4
 
5
5
  export function runStartCli(args) {
6
6
  const config = loadConfig();
@@ -1,7 +1,7 @@
1
- import { setupStatus } from "../setup/index.js";
2
- import { serviceStatus } from "../service/index.js";
3
- import { computeAppStatus } from "./app-status.js";
4
- import { printServiceSummary, printSetupSummary } from "./output.js";
1
+ import { setupStatus } from "../../setup/index.js";
2
+ import { serviceStatus } from "../../service/index.js";
3
+ import { computeAppStatus } from "../app-status.js";
4
+ import { printServiceSummary, printSetupSummary } from "../output.js";
5
5
 
6
6
  export async function appStatus({ json = false } = {}) {
7
7
  const setup = setupStatus();
package/src/cli/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { printVersion } from "./version.js";
3
3
  import { printHelp } from "./help.js";
4
- import { appStatus } from "./status-command.js";
5
- import { runConfigCli } from "./config-command.js";
6
- import { runDisableCli } from "./disable-command.js";
7
- import { runKeyCli } from "./key-command.js";
8
- import { runLoginCli } from "./login-command.js";
9
- import { runProxyCli } from "./proxy-command.js";
10
- import { runServiceCli } from "./service-command.js";
11
- import { runSetupCli } from "./setup-command.js";
12
- import { runStartCli } from "./start-command.js";
4
+ import { appStatus } from "./commands/status.js";
5
+ import { runConfigCli } from "./commands/config.js";
6
+ import { runDisableCli } from "./commands/disable.js";
7
+ import { runKeyCli } from "./commands/key.js";
8
+ import { runLoginCli } from "./commands/login.js";
9
+ import { runProxyCli } from "./commands/proxy.js";
10
+ import { runServiceCli } from "./commands/service.js";
11
+ import { runSetupCli } from "./commands/setup.js";
12
+ import { runStartCli } from "./commands/start.js";
13
13
  import { doctor, renderDoctorText } from "./doctor.js";
14
14
  import { formatProbeReport, runProbe } from "./probe.js";
15
15
 
@@ -2,6 +2,8 @@ import { logEvent } from "../shared/log.js";
2
2
  import { copyResponseHeaders } from "./headers.js";
3
3
  import { countCacheControlBlocks } from "./cache-policy.js";
4
4
 
5
+ const SSE_FRAME_BOUNDARY = /\r?\n\r?\n/;
6
+
5
7
  export function parseJsonBody(body, contentType) {
6
8
  if (!body || !contentType.includes("json")) return undefined;
7
9
  try {
@@ -19,7 +21,7 @@ export function cacheRequestFields(payload) {
19
21
  hasPromptCacheKey: payload.prompt_cache_key !== undefined,
20
22
  cacheControlBlocks: countCacheControlBlocks(payload)
21
23
  };
22
- return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined && value !== false && value !== 0));
24
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined && value !== false));
23
25
  }
24
26
 
25
27
  function responseItems(payload) {
@@ -103,8 +105,7 @@ export function createSseCacheObserver(route, requestCache) {
103
105
 
104
106
  const consume = () => {
105
107
  let match;
106
- const boundary = /\r?\n\r?\n/;
107
- while ((match = boundary.exec(buffer)) !== null) {
108
+ while ((match = SSE_FRAME_BOUNDARY.exec(buffer)) !== null) {
108
109
  const frame = buffer.slice(0, match.index);
109
110
  buffer = buffer.slice(match.index + match[0].length);
110
111
  for (const payload of sseJsonPayloads(frame)) {
@@ -1,17 +1,15 @@
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";
3
4
  import { canonicalClaudeFamily, findCompatibleModel as findCompatibleModelShared } from "./model-catalog.js";
4
5
  import { withDefaultAnthropicCache, withDefaultPromptCache } from "./cache-policy.js";
5
6
 
6
- function normalizeProxyEffort(effort) {
7
- return effort === "max" ? "xhigh" : effort;
8
- }
9
-
10
7
  function openAIEffortRoute(model, effort) {
11
8
  if (effort === undefined) return undefined;
12
- const normalized = normalizeProxyEffort(effort);
9
+ const normalized = normalizeEffort(effort);
10
+ if (!normalized) return undefined;
13
11
  if (/^gpt-5-mini(?:-|$)/.test(model) && normalized === "xhigh") return "high";
14
- if (normalized !== effort) return normalized;
12
+ if (normalized !== String(effort).trim().toLowerCase()) return normalized;
15
13
  return undefined;
16
14
  }
17
15
 
@@ -47,8 +45,8 @@ function withSupportedAnthropicEffort(payload, models) {
47
45
  const model = typeof payload?.model === "string" ? payload.model : "";
48
46
  const family = canonicalClaudeFamily(model);
49
47
  if (!family) return payload;
50
- const nextEffort = normalizeProxyEffort(effort);
51
- if (!["low", "medium", "high", "xhigh"].includes(nextEffort)) return payload;
48
+ const nextEffort = normalizeEffort(effort);
49
+ if (!nextEffort || !EFFORT_VALUES.includes(nextEffort)) return payload;
52
50
  const routed = findCompatibleModelShared({
53
51
  models,
54
52
  family,
@@ -4,7 +4,7 @@ import { logEvent } from "../shared/log.js";
4
4
  import { isResponsesWebSocketOptIn, proxyResponsesWebSocket, shouldUseResponsesWebSocket } from "./responses-websocket.js";
5
5
  import { annotateModelsResponse } from "./models.js";
6
6
  import { upstreamHeaders } from "./headers.js";
7
- import { withAnthropicDefaults, withOpenAIDefaults } from "./effort.js";
7
+ import { withAnthropicDefaults, withOpenAIDefaults } from "./effort-routing.js";
8
8
  import {
9
9
  cacheRequestFields,
10
10
  createSseCacheObserver,
@@ -1,3 +1,5 @@
1
+ import { copyResponseHeaders } from "./headers.js";
2
+
1
3
  export function aerialSupportForModel(model) {
2
4
  const endpoints = Array.isArray(model.supported_endpoints) ? model.supported_endpoints : [];
3
5
  const routes = [];
@@ -19,9 +21,9 @@ export async function annotateModelsResponse(response) {
19
21
  const contentType = response.headers.get("content-type") || "";
20
22
  if (!response.ok || !contentType.includes("application/json")) return response;
21
23
  const payload = await response.json();
22
- if (!Array.isArray(payload.data)) return Response.json(payload, { status: response.status, headers: response.headers });
24
+ if (!Array.isArray(payload.data)) return Response.json(payload, { status: response.status, headers: copyResponseHeaders(response) });
23
25
  return Response.json({
24
26
  ...payload,
25
27
  data: payload.data.map((model) => ({ ...model, aerial: aerialSupportForModel(model) }))
26
- }, { status: response.status });
28
+ }, { status: response.status, headers: copyResponseHeaders(response) });
27
29
  }
@@ -93,6 +93,7 @@ export async function proxyResponsesWebSocket(payload, headers, { initiator = "u
93
93
  };
94
94
  ws.addEventListener("message", async (event) => {
95
95
  const message = await normalizeMessageData(event.data);
96
+ if (!message) return;
96
97
  controller.enqueue(encoder.encode(toSseFrame(message)));
97
98
  if (isTerminalMessage(message)) {
98
99
  controller.close();
@@ -99,7 +99,7 @@ async function readBody(req, maxBytes = MAX_BODY_BYTES) {
99
99
 
100
100
  async function handle(fetchRequest, runtime = {}) {
101
101
  const url = new URL(fetchRequest.url);
102
- const config = loadConfig();
102
+ const config = runtime.config || loadConfig();
103
103
  if (fetchRequest.method === "GET" && url.pathname === "/health") {
104
104
  return Response.json({ ok: true, service: "aerial", host: runtime.host || config.host, port: runtime.port || config.port });
105
105
  }
@@ -157,8 +157,9 @@ export function createServer(runtime = {}) {
157
157
  try {
158
158
  logEvent("request_start", { method: req.method, path: req.url });
159
159
  let localAuthValidated = false;
160
+ let config;
160
161
  if (!publicRoute(req)) {
161
- const config = loadConfig();
162
+ config = loadConfig();
162
163
  if (!validateLocalAuth(nodeHeaderObject(req.headers), config)) {
163
164
  const fetchResponse = json(401, { error: { type: "authentication_error", message: "Invalid or missing Aerial API key" } });
164
165
  await writeNodeResponse(res, fetchResponse, controller.signal);
@@ -169,7 +170,7 @@ export function createServer(runtime = {}) {
169
170
  }
170
171
  const body = await readBody(req);
171
172
  const fetchRequest = nodeRequestToFetch(req, body, controller.signal);
172
- const fetchResponse = await handle(fetchRequest, { ...runtime, localAuthValidated });
173
+ const fetchResponse = await handle(fetchRequest, { ...runtime, localAuthValidated, config });
173
174
  await writeNodeResponse(res, fetchResponse, controller.signal);
174
175
  logEvent("request_end", { method: req.method, path: req.url, status: fetchResponse.status, ms: Date.now() - started });
175
176
  } catch (error) {
@@ -88,6 +88,7 @@ export function setupClaude({ model, effort, apiKeyHelper = DEFAULT_CLAUDE_API_K
88
88
  env: claudeEnvForAerial(current.env, config)
89
89
  };
90
90
  if (selectedModel) next.model = selectedModel;
91
+ if (normalizedEffort) next.effortLevel = normalizedEffort;
91
92
  atomicWriteFile(file, `${JSON.stringify(next, null, 2)}\n`);
92
93
  if (normalizedEffort && config.defaultEffort !== normalizedEffort) {
93
94
  saveConfig({ ...config, defaultEffort: normalizedEffort });
@@ -167,17 +168,19 @@ export function claudeStatus() {
167
168
  const expectedBaseUrl = `http://${config.host}:${config.port}`;
168
169
  const file = claudeSettingsFile();
169
170
  const backups = backupPathsFor(file);
170
- const effort = typeof config.defaultEffort === "string" && config.defaultEffort.trim() ? config.defaultEffort.trim() : "missing";
171
- if (!fs.existsSync(file)) return { target: "claude", state: "missing", file, backups, effort };
171
+ const fallbackEffort = typeof config.defaultEffort === "string" && config.defaultEffort.trim() ? config.defaultEffort.trim() : "missing";
172
+ if (!fs.existsSync(file)) return { target: "claude", state: "missing", file, backups, effort: fallbackEffort };
172
173
  let doc;
173
174
  try {
174
175
  doc = JSON.parse(fs.readFileSync(file, "utf8"));
175
176
  } catch (err) {
176
- return { target: "claude", state: "invalid", file, backups, error: err.message, effort };
177
+ return { target: "claude", state: "invalid", file, backups, error: err.message, effort: fallbackEffort };
177
178
  }
178
179
  const state = claudeStateFromDoc(doc, expectedBaseUrl);
179
180
  const model = typeof doc?.model === "string" ? doc.model : undefined;
180
181
  const baseUrl = doc?.env?.ANTHROPIC_BASE_URL;
182
+ const settingsEffort = typeof doc?.effortLevel === "string" ? normalizeEffort(doc.effortLevel) : undefined;
183
+ const effort = settingsEffort || fallbackEffort;
181
184
  return { target: "claude", state, file, backups, model, baseUrl, effort };
182
185
  }
183
186
 
@@ -22,7 +22,7 @@ export function atomicWriteFile(file, content, { mode } = {}) {
22
22
  throw err;
23
23
  }
24
24
  if (process.platform !== "win32" && mode !== undefined) {
25
- fs.chmodSync(file, mode);
25
+ try { fs.chmodSync(file, mode); } catch {}
26
26
  }
27
27
  } catch (err) {
28
28
  try { fs.unlinkSync(tmp); } catch {}