@jiayunxie/aerial 0.1.0 → 0.1.2

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/src/probe.js CHANGED
@@ -1,141 +1,141 @@
1
- import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "./copilot.js";
2
-
3
- function modelRoutes(model) {
4
- return model.aerial?.routes || [];
5
- }
6
-
7
- function firstModel(models, route) {
8
- return models.find((model) => modelRoutes(model).includes(route));
9
- }
10
-
11
- async function readJson(response) {
12
- const text = await response.text();
13
- if (!text) return {};
14
- try {
15
- return JSON.parse(text);
16
- } catch {
17
- return { raw: text };
18
- }
19
- }
20
-
21
- function tokenSummary(payload) {
22
- const usage = payload.usage || {};
23
- return {
24
- input: usage.input_tokens ?? usage.prompt_tokens,
25
- output: usage.output_tokens ?? usage.completion_tokens,
26
- cached: usage.input_tokens_details?.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? usage.cache_read_input_tokens
27
- };
28
- }
29
-
30
- async function probeRoute(name, model, handler, payload, headers = {}) {
31
- const response = await handler(new Request(`http://aerial.local/probe/${name}`, {
32
- method: "POST",
33
- headers: { "content-type": "application/json", ...headers },
34
- body: JSON.stringify(payload)
35
- }));
36
- const body = await readJson(response);
37
- return {
38
- route: name,
39
- model: model.id,
40
- ok: response.ok,
41
- status: response.status,
42
- contentType: response.headers.get("content-type") || undefined,
43
- usage: response.ok ? tokenSummary(body) : undefined,
44
- error: response.ok ? undefined : body.error || body
45
- };
46
- }
47
-
48
- function summarizeModels(models) {
49
- const summary = { responses: 0, messages: 0, chat: 0, websocketResponses: 0, embeddings: 0, unsupported: 0 };
50
- for (const model of models) {
51
- const routes = modelRoutes(model);
52
- if (routes.includes("responses")) summary.responses += 1;
53
- if (routes.includes("messages")) summary.messages += 1;
54
- if (routes.includes("chat")) summary.chat += 1;
55
- if (modelRoutes(model).includes("responses_websocket")) summary.websocketResponses += 1;
56
- if (model.aerial?.notes?.includes("embeddings_not_implemented")) summary.embeddings += 1;
57
- if (!model.aerial?.supported) summary.unsupported += 1;
58
- }
59
- return summary;
60
- }
61
-
62
- export async function runProbe({ live = false } = {}) {
63
- const modelsResponse = await proxyModels(new Request("http://aerial.local/v1/models", { method: "GET" }));
64
- const modelsPayload = await readJson(modelsResponse);
65
- if (!modelsResponse.ok) {
66
- return { ok: false, generatedAt: new Date().toISOString(), error: modelsPayload.error || modelsPayload };
67
- }
68
-
69
- const models = Array.isArray(modelsPayload.data) ? modelsPayload.data : [];
70
- const report = {
71
- ok: true,
72
- generatedAt: new Date().toISOString(),
73
- live,
74
- summary: summarizeModels(models),
75
- routes: [],
76
- models: models.map((model) => ({
77
- id: model.id,
78
- routes: modelRoutes(model),
79
- notes: model.aerial?.notes || [],
80
- supported: Boolean(model.aerial?.supported)
81
- }))
82
- };
83
-
84
- if (!live) return report;
85
-
86
- const responsesModel = firstModel(models, "responses");
87
- if (responsesModel) {
88
- report.routes.push(await probeRoute("responses", responsesModel, proxyResponses, {
89
- model: responsesModel.id,
90
- input: "Return only: aerial-probe",
91
- max_output_tokens: 16,
92
- store: false
93
- }));
94
- }
95
-
96
- const messagesModel = firstModel(models, "messages");
97
- if (messagesModel) {
98
- report.routes.push(await probeRoute("messages", messagesModel, proxyMessages, {
99
- model: messagesModel.id,
100
- max_tokens: 16,
101
- messages: [{ role: "user", content: "Return only: aerial-probe" }]
102
- }, { "anthropic-version": "2023-06-01" }));
103
- }
104
-
105
- const chatModel = firstModel(models, "chat");
106
- if (chatModel) {
107
- report.routes.push(await probeRoute("chat", chatModel, proxyChatCompletions, {
108
- model: chatModel.id,
109
- messages: [{ role: "user", content: "Return only: aerial-probe" }],
110
- max_completion_tokens: 16
111
- }));
112
- }
113
-
114
- return report;
115
- }
116
-
117
- export function formatProbeReport(report) {
118
- if (!report.ok) return `Aerial probe failed: ${JSON.stringify(report.error)}`;
119
- const lines = [
120
- `Aerial probe (${report.live ? "live" : "models"}) at ${report.generatedAt}`,
121
- `Models: ${report.models.length}`,
122
- `Routes: responses=${report.summary.responses}, responsesWebSocket=${report.summary.websocketResponses}, messages=${report.summary.messages}, chat=${report.summary.chat}`,
123
- `Unsupported: embeddings=${report.summary.embeddings}, noRoute=${report.summary.unsupported}`
124
- ];
125
-
126
- if (report.routes.length) {
127
- lines.push("", "Live route checks:");
128
- for (const route of report.routes) {
129
- const usage = route.usage ? ` input=${route.usage.input ?? "?"} output=${route.usage.output ?? "?"} cached=${route.usage.cached ?? 0}` : "";
130
- lines.push(`- ${route.route}: ${route.ok ? "ok" : "fail"} status=${route.status} model=${route.model}${usage}`);
131
- }
132
- }
133
-
134
- lines.push("", "Model matrix:");
135
- for (const model of report.models) {
136
- const routes = model.routes.length ? model.routes.join(",") : "-";
137
- const notes = model.notes.length ? ` notes=${model.notes.join(",")}` : "";
138
- lines.push(`- ${model.id}: routes=${routes}${notes}`);
139
- }
140
- return lines.join("\n");
141
- }
1
+ import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses } from "./copilot.js";
2
+
3
+ function modelRoutes(model) {
4
+ return model.aerial?.routes || [];
5
+ }
6
+
7
+ function firstModel(models, route) {
8
+ return models.find((model) => modelRoutes(model).includes(route));
9
+ }
10
+
11
+ async function readJson(response) {
12
+ const text = await response.text();
13
+ if (!text) return {};
14
+ try {
15
+ return JSON.parse(text);
16
+ } catch {
17
+ return { raw: text };
18
+ }
19
+ }
20
+
21
+ function tokenSummary(payload) {
22
+ const usage = payload.usage || {};
23
+ return {
24
+ input: usage.input_tokens ?? usage.prompt_tokens,
25
+ output: usage.output_tokens ?? usage.completion_tokens,
26
+ cached: usage.input_tokens_details?.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? usage.cache_read_input_tokens
27
+ };
28
+ }
29
+
30
+ async function probeRoute(name, model, handler, payload, headers = {}) {
31
+ const response = await handler(new Request(`http://aerial.local/probe/${name}`, {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json", ...headers },
34
+ body: JSON.stringify(payload)
35
+ }));
36
+ const body = await readJson(response);
37
+ return {
38
+ route: name,
39
+ model: model.id,
40
+ ok: response.ok,
41
+ status: response.status,
42
+ contentType: response.headers.get("content-type") || undefined,
43
+ usage: response.ok ? tokenSummary(body) : undefined,
44
+ error: response.ok ? undefined : body.error || body
45
+ };
46
+ }
47
+
48
+ function summarizeModels(models) {
49
+ const summary = { responses: 0, messages: 0, chat: 0, websocketResponses: 0, embeddings: 0, unsupported: 0 };
50
+ for (const model of models) {
51
+ const routes = modelRoutes(model);
52
+ if (routes.includes("responses")) summary.responses += 1;
53
+ if (routes.includes("messages")) summary.messages += 1;
54
+ if (routes.includes("chat")) summary.chat += 1;
55
+ if (modelRoutes(model).includes("responses_websocket")) summary.websocketResponses += 1;
56
+ if (model.aerial?.notes?.includes("embeddings_not_implemented")) summary.embeddings += 1;
57
+ if (!model.aerial?.supported) summary.unsupported += 1;
58
+ }
59
+ return summary;
60
+ }
61
+
62
+ export async function runProbe({ live = false } = {}) {
63
+ const modelsResponse = await proxyModels(new Request("http://aerial.local/v1/models", { method: "GET" }));
64
+ const modelsPayload = await readJson(modelsResponse);
65
+ if (!modelsResponse.ok) {
66
+ return { ok: false, generatedAt: new Date().toISOString(), error: modelsPayload.error || modelsPayload };
67
+ }
68
+
69
+ const models = Array.isArray(modelsPayload.data) ? modelsPayload.data : [];
70
+ const report = {
71
+ ok: true,
72
+ generatedAt: new Date().toISOString(),
73
+ live,
74
+ summary: summarizeModels(models),
75
+ routes: [],
76
+ models: models.map((model) => ({
77
+ id: model.id,
78
+ routes: modelRoutes(model),
79
+ notes: model.aerial?.notes || [],
80
+ supported: Boolean(model.aerial?.supported)
81
+ }))
82
+ };
83
+
84
+ if (!live) return report;
85
+
86
+ const responsesModel = firstModel(models, "responses");
87
+ if (responsesModel) {
88
+ report.routes.push(await probeRoute("responses", responsesModel, proxyResponses, {
89
+ model: responsesModel.id,
90
+ input: "Return only: aerial-probe",
91
+ max_output_tokens: 16,
92
+ store: false
93
+ }));
94
+ }
95
+
96
+ const messagesModel = firstModel(models, "messages");
97
+ if (messagesModel) {
98
+ report.routes.push(await probeRoute("messages", messagesModel, proxyMessages, {
99
+ model: messagesModel.id,
100
+ max_tokens: 16,
101
+ messages: [{ role: "user", content: "Return only: aerial-probe" }]
102
+ }, { "anthropic-version": "2023-06-01" }));
103
+ }
104
+
105
+ const chatModel = firstModel(models, "chat");
106
+ if (chatModel) {
107
+ report.routes.push(await probeRoute("chat", chatModel, proxyChatCompletions, {
108
+ model: chatModel.id,
109
+ messages: [{ role: "user", content: "Return only: aerial-probe" }],
110
+ max_completion_tokens: 16
111
+ }));
112
+ }
113
+
114
+ return report;
115
+ }
116
+
117
+ export function formatProbeReport(report) {
118
+ if (!report.ok) return `Aerial probe failed: ${JSON.stringify(report.error)}`;
119
+ const lines = [
120
+ `Aerial probe (${report.live ? "live" : "models"}) at ${report.generatedAt}`,
121
+ `Models: ${report.models.length}`,
122
+ `Routes: responses=${report.summary.responses}, responsesWebSocket=${report.summary.websocketResponses}, messages=${report.summary.messages}, chat=${report.summary.chat}`,
123
+ `Unsupported: embeddings=${report.summary.embeddings}, noRoute=${report.summary.unsupported}`
124
+ ];
125
+
126
+ if (report.routes.length) {
127
+ lines.push("", "Live route checks:");
128
+ for (const route of report.routes) {
129
+ const usage = route.usage ? ` input=${route.usage.input ?? "?"} output=${route.usage.output ?? "?"} cached=${route.usage.cached ?? 0}` : "";
130
+ lines.push(`- ${route.route}: ${route.ok ? "ok" : "fail"} status=${route.status} model=${route.model}${usage}`);
131
+ }
132
+ }
133
+
134
+ lines.push("", "Model matrix:");
135
+ for (const model of report.models) {
136
+ const routes = model.routes.length ? model.routes.join(",") : "-";
137
+ const notes = model.notes.length ? ` notes=${model.notes.join(",")}` : "";
138
+ lines.push(`- ${model.id}: routes=${routes}${notes}`);
139
+ }
140
+ return lines.join("\n");
141
+ }
package/src/server.js CHANGED
@@ -1,101 +1,101 @@
1
- import http from "node:http";
2
- import { DEFAULT_HOST, DEFAULT_PORT } from "./constants.js";
3
- import { loadConfig, validateLocalAuth } from "./config.js";
4
- import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses, localCountTokens } from "./copilot.js";
5
- import { logEvent } from "./log.js";
6
-
7
- function json(status, body) {
8
- return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
9
- }
10
-
11
- function nodeRequestToFetch(req, body, signal) {
12
- return new Request(`http://${req.headers.host}${req.url}`, { method: req.method, headers: req.headers, body: body.length ? body : undefined, duplex: "half", signal });
13
- }
14
-
15
- async function readBody(req) {
16
- const chunks = [];
17
- for await (const chunk of req) chunks.push(chunk);
18
- return Buffer.concat(chunks);
19
- }
20
-
21
- async function handle(fetchRequest, runtime = {}) {
22
- const url = new URL(fetchRequest.url);
23
- const config = loadConfig();
24
- if (fetchRequest.method === "GET" && url.pathname === "/health") {
25
- return Response.json({ ok: true, service: "aerial", host: runtime.host || config.host, port: runtime.port || config.port });
26
- }
27
-
28
- if (!validateLocalAuth(Object.fromEntries(fetchRequest.headers), config)) {
29
- return json(401, { error: { type: "authentication_error", message: "Invalid or missing Aerial API key" } });
30
- }
31
-
32
- if (fetchRequest.method === "GET" && url.pathname === "/v1/models") return proxyModels(fetchRequest);
33
- if (fetchRequest.method === "POST" && url.pathname === "/v1/responses") return proxyResponses(fetchRequest);
34
- if (fetchRequest.method === "POST" && url.pathname === "/v1/messages") return proxyMessages(fetchRequest);
35
- if (fetchRequest.method === "POST" && url.pathname === "/v1/messages/count_tokens") return localCountTokens(fetchRequest);
36
- if (fetchRequest.method === "POST" && url.pathname === "/v1/chat/completions") return proxyChatCompletions(fetchRequest);
37
- return json(404, { error: { type: "not_found", message: `No route for ${fetchRequest.method} ${url.pathname}` } });
38
- }
39
-
40
- async function writeNodeResponse(res, fetchResponse, signal) {
41
- res.statusCode = fetchResponse.status;
42
- fetchResponse.headers.forEach((value, key) => res.setHeader(key, value));
43
- if (!fetchResponse.body) {
44
- res.end();
45
- return;
46
- }
47
- const reader = fetchResponse.body.getReader();
48
- try {
49
- while (!signal?.aborted) {
50
- const { done, value } = await reader.read();
51
- if (done) break;
52
- if (!res.write(Buffer.from(value))) {
53
- await new Promise((resolve) => res.once("drain", resolve));
54
- }
55
- }
56
- } finally {
57
- if (signal?.aborted) await reader.cancel().catch(() => {});
58
- }
59
- if (!res.destroyed) res.end();
60
- }
61
-
62
- export function createServer(runtime = {}) {
63
- const server = http.createServer(async (req, res) => {
64
- const started = Date.now();
65
- const controller = new AbortController();
66
- res.on("close", () => controller.abort());
67
- try {
68
- logEvent("request_start", { method: req.method, path: req.url });
69
- const body = await readBody(req);
70
- const fetchRequest = nodeRequestToFetch(req, body, controller.signal);
71
- const fetchResponse = await handle(fetchRequest, runtime);
72
- await writeNodeResponse(res, fetchResponse, controller.signal);
73
- logEvent("request_end", { method: req.method, path: req.url, status: fetchResponse.status, ms: Date.now() - started });
74
- } catch (error) {
75
- if (error.name === "AbortError" || res.destroyed) {
76
- logEvent("request_aborted", { method: req.method, path: req.url, ms: Date.now() - started });
77
- return;
78
- }
79
- logEvent("request_end", { method: req.method, path: req.url, status: 500, ms: Date.now() - started, error: error.message });
80
- res.statusCode = error.message?.includes("Missing GitHub token") ? 503 : 500;
81
- res.setHeader("content-type", "application/json");
82
- res.end(JSON.stringify({ error: { type: "aerial_error", message: error.message } }));
83
- }
84
- });
85
-
86
- server.on("upgrade", (req, socket) => {
87
- logEvent("websocket_unsupported", { path: req.url });
88
- socket.end("HTTP/1.1 501 Not Implemented\r\ncontent-type: application/json\r\nconnection: close\r\n\r\n{\"error\":{\"type\":\"not_implemented\",\"message\":\"Aerial does not expose a client WebSocket. Use HTTP POST /v1/responses; the upstream ws:/responses path is an internal opt-in transport (AERIAL_RESPONSES_WEBSOCKET=on) and is never proxied to clients.\"}}");
89
- });
90
-
91
- return server;
92
- }
93
-
94
- export function startServer({ host, port } = {}) {
95
- const config = loadConfig();
96
- const bindHost = host || config.host || DEFAULT_HOST;
97
- const bindPort = Number(port || config.port || DEFAULT_PORT);
98
- const server = createServer({ host: bindHost, port: bindPort });
99
- server.listen(bindPort, bindHost, () => logEvent("server_start", { host: bindHost, port: bindPort }));
100
- return server;
101
- }
1
+ import http from "node:http";
2
+ import { DEFAULT_HOST, DEFAULT_PORT } from "./constants.js";
3
+ import { loadConfig, validateLocalAuth } from "./config.js";
4
+ import { proxyChatCompletions, proxyMessages, proxyModels, proxyResponses, localCountTokens } from "./copilot.js";
5
+ import { logEvent } from "./log.js";
6
+
7
+ function json(status, body) {
8
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
9
+ }
10
+
11
+ function nodeRequestToFetch(req, body, signal) {
12
+ return new Request(`http://${req.headers.host}${req.url}`, { method: req.method, headers: req.headers, body: body.length ? body : undefined, duplex: "half", signal });
13
+ }
14
+
15
+ async function readBody(req) {
16
+ const chunks = [];
17
+ for await (const chunk of req) chunks.push(chunk);
18
+ return Buffer.concat(chunks);
19
+ }
20
+
21
+ async function handle(fetchRequest, runtime = {}) {
22
+ const url = new URL(fetchRequest.url);
23
+ const config = loadConfig();
24
+ if (fetchRequest.method === "GET" && url.pathname === "/health") {
25
+ return Response.json({ ok: true, service: "aerial", host: runtime.host || config.host, port: runtime.port || config.port });
26
+ }
27
+
28
+ if (!validateLocalAuth(Object.fromEntries(fetchRequest.headers), config)) {
29
+ return json(401, { error: { type: "authentication_error", message: "Invalid or missing Aerial API key" } });
30
+ }
31
+
32
+ if (fetchRequest.method === "GET" && url.pathname === "/v1/models") return proxyModels(fetchRequest);
33
+ if (fetchRequest.method === "POST" && url.pathname === "/v1/responses") return proxyResponses(fetchRequest);
34
+ if (fetchRequest.method === "POST" && url.pathname === "/v1/messages") return proxyMessages(fetchRequest);
35
+ if (fetchRequest.method === "POST" && url.pathname === "/v1/messages/count_tokens") return localCountTokens(fetchRequest);
36
+ if (fetchRequest.method === "POST" && url.pathname === "/v1/chat/completions") return proxyChatCompletions(fetchRequest);
37
+ return json(404, { error: { type: "not_found", message: `No route for ${fetchRequest.method} ${url.pathname}` } });
38
+ }
39
+
40
+ async function writeNodeResponse(res, fetchResponse, signal) {
41
+ res.statusCode = fetchResponse.status;
42
+ fetchResponse.headers.forEach((value, key) => res.setHeader(key, value));
43
+ if (!fetchResponse.body) {
44
+ res.end();
45
+ return;
46
+ }
47
+ const reader = fetchResponse.body.getReader();
48
+ try {
49
+ while (!signal?.aborted) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+ if (!res.write(Buffer.from(value))) {
53
+ await new Promise((resolve) => res.once("drain", resolve));
54
+ }
55
+ }
56
+ } finally {
57
+ if (signal?.aborted) await reader.cancel().catch(() => {});
58
+ }
59
+ if (!res.destroyed) res.end();
60
+ }
61
+
62
+ export function createServer(runtime = {}) {
63
+ const server = http.createServer(async (req, res) => {
64
+ const started = Date.now();
65
+ const controller = new AbortController();
66
+ res.on("close", () => controller.abort());
67
+ try {
68
+ logEvent("request_start", { method: req.method, path: req.url });
69
+ const body = await readBody(req);
70
+ const fetchRequest = nodeRequestToFetch(req, body, controller.signal);
71
+ const fetchResponse = await handle(fetchRequest, runtime);
72
+ await writeNodeResponse(res, fetchResponse, controller.signal);
73
+ logEvent("request_end", { method: req.method, path: req.url, status: fetchResponse.status, ms: Date.now() - started });
74
+ } catch (error) {
75
+ if (error.name === "AbortError" || res.destroyed) {
76
+ logEvent("request_aborted", { method: req.method, path: req.url, ms: Date.now() - started });
77
+ return;
78
+ }
79
+ logEvent("request_end", { method: req.method, path: req.url, status: 500, ms: Date.now() - started, error: error.message });
80
+ res.statusCode = error.message?.includes("Missing GitHub token") ? 503 : 500;
81
+ res.setHeader("content-type", "application/json");
82
+ res.end(JSON.stringify({ error: { type: "aerial_error", message: error.message } }));
83
+ }
84
+ });
85
+
86
+ server.on("upgrade", (req, socket) => {
87
+ logEvent("websocket_unsupported", { path: req.url });
88
+ socket.end("HTTP/1.1 501 Not Implemented\r\ncontent-type: application/json\r\nconnection: close\r\n\r\n{\"error\":{\"type\":\"not_implemented\",\"message\":\"Aerial does not expose a client WebSocket. Use HTTP POST /v1/responses; the upstream ws:/responses path is an internal opt-in transport (AERIAL_RESPONSES_WEBSOCKET=on) and is never proxied to clients.\"}}");
89
+ });
90
+
91
+ return server;
92
+ }
93
+
94
+ export function startServer({ host, port } = {}) {
95
+ const config = loadConfig();
96
+ const bindHost = host || config.host || DEFAULT_HOST;
97
+ const bindPort = Number(port || config.port || DEFAULT_PORT);
98
+ const server = createServer({ host: bindHost, port: bindPort });
99
+ server.listen(bindPort, bindHost, () => logEvent("server_start", { host: bindHost, port: bindPort }));
100
+ return server;
101
+ }
package/src/setup.js CHANGED
@@ -6,40 +6,40 @@ import { ensureApiKey, loadConfig } from "./config.js";
6
6
  import { logEvent } from "./log.js";
7
7
 
8
8
  const AERIAL_ENV_KEY = "AERIAL_API_KEY";
9
-
10
- function backupIfExists(file) {
11
- if (!fs.existsSync(file)) return undefined;
12
- const backup = `${file}.aerial-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
13
- fs.copyFileSync(file, backup);
14
- return backup;
15
- }
16
-
17
- function ensureParent(file) {
18
- fs.mkdirSync(path.dirname(file), { recursive: true });
19
- }
20
-
21
- function setTomlString(content, key, value) {
22
- const line = `${key} = "${value}"`;
23
- const re = new RegExp(`^${key}\\s*=.*$`, "m");
24
- return re.test(content) ? content.replace(re, line) : `${content.trimEnd()}\n${line}\n`;
25
- }
26
-
9
+
10
+ function backupIfExists(file) {
11
+ if (!fs.existsSync(file)) return undefined;
12
+ const backup = `${file}.aerial-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
13
+ fs.copyFileSync(file, backup);
14
+ return backup;
15
+ }
16
+
17
+ function ensureParent(file) {
18
+ fs.mkdirSync(path.dirname(file), { recursive: true });
19
+ }
20
+
21
+ function setTomlString(content, key, value) {
22
+ const line = `${key} = "${value}"`;
23
+ const re = new RegExp(`^${key}\\s*=.*$`, "m");
24
+ return re.test(content) ? content.replace(re, line) : `${content.trimEnd()}\n${line}\n`;
25
+ }
26
+
27
27
  function upsertTomlSection(content, section, values) {
28
- const heading = `[${section}]`;
29
- const lines = Object.entries(values).map(([key, value]) => `${key} = "${value}"`).join("\n");
30
- const block = `${heading}\n${lines}\n`;
31
- const source = content.split(/\r?\n/);
32
- const start = source.findIndex((line) => line.trim() === heading);
33
- if (start === -1) return `${content.trimEnd()}\n\n${block}`;
34
- let end = source.length;
35
- for (let i = start + 1; i < source.length; i += 1) {
36
- if (/^\s*\[.*\]\s*$/.test(source[i])) {
37
- end = i;
38
- break;
39
- }
40
- }
41
- source.splice(start, end - start, ...block.trimEnd().split("\n"));
42
- return `${source.join("\n").trimEnd()}\n`;
28
+ const heading = `[${section}]`;
29
+ const lines = Object.entries(values).map(([key, value]) => `${key} = "${value}"`).join("\n");
30
+ const block = `${heading}\n${lines}\n`;
31
+ const source = content.split(/\r?\n/);
32
+ const start = source.findIndex((line) => line.trim() === heading);
33
+ if (start === -1) return `${content.trimEnd()}\n\n${block}`;
34
+ let end = source.length;
35
+ for (let i = start + 1; i < source.length; i += 1) {
36
+ if (/^\s*\[.*\]\s*$/.test(source[i])) {
37
+ end = i;
38
+ break;
39
+ }
40
+ }
41
+ source.splice(start, end - start, ...block.trimEnd().split("\n"));
42
+ return `${source.join("\n").trimEnd()}\n`;
43
43
  }
44
44
 
45
45
  function persistUserEnv(name, value) {
@@ -81,19 +81,19 @@ export function setupCodex({ model } = {}) {
81
81
  const apiKey = ensureClientApiKeyEnv();
82
82
  const config = loadConfig();
83
83
  const selectedModel = model || config.defaultModel || "gpt-4.1";
84
- const file = path.join(os.homedir(), ".codex", "config.toml");
85
- ensureParent(file);
86
- const backup = backupIfExists(file);
87
- let content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
88
- content = setTomlString(content, "model_provider", "aerial");
89
- content = setTomlString(content, "model", selectedModel);
90
- content = upsertTomlSection(content, "model_providers.aerial", {
84
+ const file = path.join(os.homedir(), ".codex", "config.toml");
85
+ ensureParent(file);
86
+ const backup = backupIfExists(file);
87
+ let content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
88
+ content = setTomlString(content, "model_provider", "aerial");
89
+ content = setTomlString(content, "model", selectedModel);
90
+ content = upsertTomlSection(content, "model_providers.aerial", {
91
91
  name: "Aerial Copilot Local",
92
92
  base_url: `http://${config.host}:${config.port}/v1`,
93
93
  wire_api: "responses",
94
94
  env_key: AERIAL_ENV_KEY
95
95
  });
96
- content = upsertTomlSection(content, "profiles.aerial", { model_provider: "aerial", model: selectedModel });
96
+ content = upsertTomlSection(content, "profiles.aerial", { model_provider: "aerial", model: selectedModel });
97
97
  fs.writeFileSync(file, content, "utf8");
98
98
  logEvent("setup_write", { target: "codex", file, backup, env: apiKey.env });
99
99
  return { file, backup, model: selectedModel, env: apiKey.env };
@@ -118,4 +118,4 @@ export function setupClaude({ model } = {}) {
118
118
  logEvent("setup_write", { target: "claude", file, backup, model: selectedModel });
119
119
  return { file, backup, model: selectedModel };
120
120
  }
121
-
121
+