@auroraflow/code 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,7 @@ aurora update-clients
38
38
 
39
39
  ```bash
40
40
  npm run check
41
+ scripts/release_npm.sh 0.0.7
41
42
  node bin/aurora.js
42
43
  node bin/aurora.js init
43
44
  node bin/aurora.js claude
File without changes
File without changes
package/bin/aurora.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@auroraflow/code",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "Aurora launcher and sidecar for official Codex and Claude Code clients.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/auroraflowintl/aurora-code.git"
9
+ },
6
10
  "bin": {
7
11
  "aurora": "bin/aurora.js",
8
12
  "aurora-claude": "bin/aurora-claude.js",
@@ -11,7 +15,8 @@
11
15
  },
12
16
  "scripts": {
13
17
  "postinstall": "node bin/aurora-postinstall.js",
14
- "check": "find bin lib packages -name '*.js' -print0 | xargs -0 -n1 node --check"
18
+ "check": "find bin lib packages -name '*.js' -print0 | xargs -0 -n1 node --check",
19
+ "test": "node --test packages/protocol/src/index.test.js"
15
20
  },
16
21
  "engines": {
17
22
  "node": ">=24"
@@ -36,6 +36,9 @@ export async function createClientLaunchSpec(client, args = []) {
36
36
  CLAUDE_CONFIG_DIR: CLAUDE_HOME,
37
37
  ANTHROPIC_BASE_URL: sidecar.baseURL,
38
38
  ANTHROPIC_AUTH_TOKEN: sidecar.token,
39
+ // Claude Code v2.1.36+ sends attribution header by default.
40
+ // Keep request prefixes stable for DeepSeek prompt-cache reuse.
41
+ CLAUDE_CODE_ATTRIBUTION_HEADER: process.env.CLAUDE_CODE_ATTRIBUTION_HEADER ?? "0",
39
42
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
40
43
  CLAUDE_CODE_DISABLE_TELEMETRY: "1",
41
44
  DISABLE_AUTOUPDATER: "1"
@@ -161,8 +164,11 @@ async function ensureSidecar() {
161
164
 
162
165
  async function writeCodexRuntimeFiles({ sidecar, model }) {
163
166
  await mkdir(CODEX_HOME, { recursive: true });
167
+ const modelCatalogPath = join(CODEX_HOME, "model_catalog.json");
168
+ await writeCodexModelCatalog(sidecar, modelCatalogPath);
164
169
  const config = `model = ${tomlString(model)}
165
170
  model_provider = "aurora"
171
+ model_catalog_json = ${tomlString(modelCatalogPath)}
166
172
  suppress_unstable_features_warning = true
167
173
 
168
174
  [model_providers.aurora]
@@ -184,6 +190,32 @@ stream_idle_timeout_ms = 300000
184
190
  await writeFile(join(CODEX_HOME, "auth.json"), `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
185
191
  }
186
192
 
193
+ async function writeCodexModelCatalog(sidecar, modelCatalogPath) {
194
+ const clientVersion = "0.137.0";
195
+ const response = await fetch(
196
+ `${sidecar.baseURL}/v1/models?client_version=${encodeURIComponent(clientVersion)}`,
197
+ {
198
+ headers: { authorization: `Bearer ${sidecar.token}` },
199
+ signal: AbortSignal.timeout(5000)
200
+ }
201
+ );
202
+ if (!response.ok) {
203
+ throw new Error(`Aurora Codex model catalog request failed with HTTP ${response.status}`);
204
+ }
205
+ const payload = await response.json();
206
+ if (!Array.isArray(payload.models) || payload.models.length === 0) {
207
+ throw new Error("Aurora Codex model catalog is empty");
208
+ }
209
+ const catalog = { models: payload.models };
210
+ const cache = {
211
+ fetched_at: new Date().toISOString(),
212
+ client_version: clientVersion,
213
+ models: payload.models
214
+ };
215
+ await writeFile(modelCatalogPath, `${JSON.stringify(catalog, null, 2)}\n`, { mode: 0o600 });
216
+ await writeFile(join(CODEX_HOME, "models_cache.json"), `${JSON.stringify(cache, null, 2)}\n`, { mode: 0o600 });
217
+ }
218
+
187
219
  async function waitForSidecar(info) {
188
220
  const deadline = Date.now() + 2500;
189
221
  while (Date.now() < deadline) {
@@ -10,17 +10,26 @@ export function selectedKeyHeaders(state) {
10
10
  return key ? { authorization: `Bearer ${key}`, "x-api-key": key } : {};
11
11
  }
12
12
 
13
- export function rewriteRuntimeModel(raw, selectedAlias) {
14
- if (!selectedAlias || !raw.trim()) return raw;
13
+ export function rewriteRuntimeModel(raw, selectedAlias, capabilitiesByAlias = {}) {
14
+ if (!raw.trim()) return raw;
15
15
  try {
16
16
  const parsed = JSON.parse(raw);
17
- parsed.model = selectedAlias;
17
+ if (selectedAlias) parsed.model = selectedAlias;
18
+ filterHostedWebSearch(parsed, capabilitiesByAlias);
18
19
  return JSON.stringify(parsed);
19
20
  } catch {
20
21
  return raw;
21
22
  }
22
23
  }
23
24
 
25
+ function filterHostedWebSearch(payload, capabilitiesByAlias) {
26
+ const model = String(payload.model ?? "");
27
+ const supportsWebSearch = capabilitiesByAlias[model]?.supports_web_search === true;
28
+ if (supportsWebSearch || !Array.isArray(payload.tools)) return;
29
+ payload.tools = payload.tools.filter(tool => tool?.type !== "web_search");
30
+ if (payload.tools.length === 0) delete payload.tools;
31
+ }
32
+
24
33
  export function toClientModelItem(item) {
25
34
  const provider = String(item.provider ?? "aurora").replace(/[^a-zA-Z0-9_-]/g, "-");
26
35
  const modelID = encodeURIComponent(String(item.model_id ?? item.alias ?? item.id));
@@ -35,6 +44,124 @@ export function toClientModelItem(item) {
35
44
  };
36
45
  }
37
46
 
47
+ export function toCodexModelInfo(item, priority = 0) {
48
+ const alias = String(item.alias ?? item.id ?? item.model_id ?? "");
49
+ const displayName = item.display_name || `${item.provider ?? "aurora"} / ${item.model_id ?? alias}`;
50
+ const providerName = providerDisplayName(item.provider);
51
+ const supportsThinking = Boolean(item.supports_thinking);
52
+ const supportsTools = Boolean(item.supports_tools);
53
+ const supportsImages = Boolean(item.supports_images);
54
+ const supportsWebSearch = Boolean(item.supports_web_search);
55
+ const contextWindow = positiveNumber(item.context_window ?? item.context_window_tokens, 256000);
56
+ const maxContextWindow = positiveNumber(item.max_context_window, contextWindow);
57
+ const reasoningLevels = Array.isArray(item.supported_reasoning_levels)
58
+ ? item.supported_reasoning_levels
59
+ : defaultReasoningLevels(supportsThinking);
60
+ const modalities = Array.isArray(item.input_modalities)
61
+ ? item.input_modalities
62
+ : Array.isArray(item.modalities)
63
+ ? item.modalities
64
+ : supportsImages
65
+ ? ["text", "image"]
66
+ : ["text"];
67
+ const applyPatchToolType = Object.hasOwn(item, "apply_patch_tool_type")
68
+ ? item.apply_patch_tool_type
69
+ : supportsTools
70
+ ? "freeform"
71
+ : null;
72
+ return {
73
+ slug: alias,
74
+ display_name: displayName,
75
+ description: item.description || `${providerName} Provider`,
76
+ default_reasoning_level: item.default_reasoning_level ?? (supportsThinking ? "medium" : null),
77
+ supported_reasoning_levels: reasoningLevels,
78
+ shell_type: item.shell_type ?? "shell_command",
79
+ visibility: item.visibility ?? "list",
80
+ supported_in_api: item.supported_in_api ?? true,
81
+ priority: Number(item.priority ?? priority + 1),
82
+ additional_speed_tiers: Array.isArray(item.additional_speed_tiers) ? item.additional_speed_tiers : [],
83
+ service_tiers: Array.isArray(item.service_tiers) ? item.service_tiers : [],
84
+ default_service_tier: item.default_service_tier ?? null,
85
+ availability_nux: item.availability_nux ?? null,
86
+ upgrade: item.upgrade ?? null,
87
+ base_instructions: item.base_instructions || "You are a coding model routed through Aurora sidecar.",
88
+ model_messages: item.model_messages ?? null,
89
+ supports_reasoning_summaries: item.supports_reasoning_summaries ?? supportsThinking,
90
+ default_reasoning_summary: item.default_reasoning_summary ?? "none",
91
+ support_verbosity: item.support_verbosity ?? false,
92
+ default_verbosity: Object.hasOwn(item, "default_verbosity") ? item.default_verbosity : null,
93
+ apply_patch_tool_type: applyPatchToolType,
94
+ web_search_tool_type: Object.hasOwn(item, "web_search_tool_type") && item.web_search_tool_type
95
+ ? item.web_search_tool_type
96
+ : supportsWebSearch
97
+ ? (supportsImages ? "text_and_image" : "text")
98
+ : "text",
99
+ truncation_policy: item.truncation_policy ?? { mode: "tokens", limit: 10000 },
100
+ supports_parallel_tool_calls: item.supports_parallel_tool_calls ?? supportsTools,
101
+ supports_image_detail_original: item.supports_image_detail_original ?? supportsImages,
102
+ context_window: contextWindow,
103
+ max_context_window: maxContextWindow,
104
+ auto_compact_token_limit: item.auto_compact_token_limit ?? null,
105
+ effective_context_window_percent: positiveNumber(item.effective_context_window_percent, 95),
106
+ experimental_supported_tools: Array.isArray(item.experimental_supported_tools)
107
+ ? item.experimental_supported_tools
108
+ : [],
109
+ input_modalities: modalities,
110
+ supports_search_tool: item.supports_search_tool ?? supportsWebSearch,
111
+ auto_review_model_override: item.auto_review_model_override ?? null,
112
+ tool_mode: item.tool_mode ?? null,
113
+ multi_agent_version: item.multi_agent_version ?? null
114
+ };
115
+ }
116
+
117
+ function positiveNumber(value, defaultValue) {
118
+ const parsed = Number(value);
119
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
120
+ }
121
+
122
+ function defaultReasoningLevels(supportsThinking) {
123
+ if (!supportsThinking) return [];
124
+ return [
125
+ { effort: "minimal", description: "Fastest response with shallow reasoning" },
126
+ { effort: "low", description: "Lower latency and lighter reasoning depth" },
127
+ { effort: "medium", description: "Balanced reasoning quality and speed" },
128
+ { effort: "high", description: "Deep reasoning for complex coding tasks" },
129
+ { effort: "xhigh", description: "Maximum reasoning depth for hardest tasks" }
130
+ ];
131
+ }
132
+
133
+ function providerDisplayName(provider) {
134
+ const raw = String(provider ?? "Aurora").trim().toLowerCase();
135
+ if (!raw) return "Aurora";
136
+ const known = {
137
+ antigravity: "Antigravity",
138
+ anthropic: "Anthropic",
139
+ claude: "Claude",
140
+ codex: "Codex",
141
+ cursor: "Cursor",
142
+ deepseek: "DeepSeek",
143
+ gemini: "Gemini",
144
+ google: "Google",
145
+ kimi: "Kimi",
146
+ kiro: "Kiro",
147
+ minimax: "MiniMax",
148
+ openai: "OpenAI",
149
+ openrouter: "OpenRouter",
150
+ qwen: "Qwen",
151
+ siliconflow: "SiliconFlow",
152
+ volcengine_ark: "Volcengine Ark",
153
+ windsurf: "Windsurf",
154
+ xai: "xAI",
155
+ zhipu: "Zhipu"
156
+ };
157
+ if (known[raw]) return known[raw];
158
+ return raw
159
+ .split(/[-_\s]+/)
160
+ .filter(Boolean)
161
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
162
+ .join(" ");
163
+ }
164
+
38
165
  export function isRuntimePath(pathname) {
39
166
  return pathname === "/v1/messages" ||
40
167
  pathname === "/v1/responses" ||
@@ -0,0 +1,180 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { rewriteRuntimeModel, toCodexModelInfo } from "./index.js";
5
+
6
+ const CODEX_MODEL_INFO_FIELDS = [
7
+ "additional_speed_tiers",
8
+ "apply_patch_tool_type",
9
+ "auto_compact_token_limit",
10
+ "auto_review_model_override",
11
+ "availability_nux",
12
+ "base_instructions",
13
+ "context_window",
14
+ "default_reasoning_level",
15
+ "default_reasoning_summary",
16
+ "default_service_tier",
17
+ "default_verbosity",
18
+ "description",
19
+ "display_name",
20
+ "effective_context_window_percent",
21
+ "experimental_supported_tools",
22
+ "input_modalities",
23
+ "max_context_window",
24
+ "model_messages",
25
+ "multi_agent_version",
26
+ "priority",
27
+ "service_tiers",
28
+ "shell_type",
29
+ "slug",
30
+ "support_verbosity",
31
+ "supported_in_api",
32
+ "supported_reasoning_levels",
33
+ "supports_image_detail_original",
34
+ "supports_parallel_tool_calls",
35
+ "supports_reasoning_summaries",
36
+ "supports_search_tool",
37
+ "tool_mode",
38
+ "truncation_policy",
39
+ "upgrade",
40
+ "visibility",
41
+ "web_search_tool_type"
42
+ ].sort();
43
+
44
+ test("projects published reasoning and tool capabilities into Codex metadata", () => {
45
+ const model = toCodexModelInfo({
46
+ alias: "deepseek/deepseek-v4-flash",
47
+ provider: "deepseek",
48
+ model_id: "deepseek-v4-flash",
49
+ context_window_tokens: 1048576,
50
+ supports_tools: true,
51
+ supports_images: false,
52
+ supports_thinking: true,
53
+ supports_web_search: false,
54
+ default_reasoning_level: "high",
55
+ supported_reasoning_levels: [
56
+ { effort: "minimal", description: "off" },
57
+ { effort: "high", description: "high" },
58
+ { effort: "xhigh", description: "max" }
59
+ ],
60
+ supports_reasoning_summaries: true,
61
+ shell_type: "shell_command",
62
+ truncation_policy: { mode: "tokens", limit: 10000 },
63
+ supports_parallel_tool_calls: true,
64
+ tool_mode: null,
65
+ multi_agent_version: null
66
+ }, 4);
67
+
68
+ assert.equal(model.slug, "deepseek/deepseek-v4-flash");
69
+ assert.equal(model.priority, 5);
70
+ assert.equal(model.supports_reasoning_summaries, true);
71
+ assert.equal(model.default_reasoning_level, "high");
72
+ assert.deepEqual(model.supported_reasoning_levels.map(level => level.effort), [
73
+ "minimal",
74
+ "high",
75
+ "xhigh"
76
+ ]);
77
+ assert.equal(model.shell_type, "shell_command");
78
+ assert.equal(model.supports_parallel_tool_calls, true);
79
+ assert.equal(model.apply_patch_tool_type, "freeform");
80
+ assert.equal(model.supports_search_tool, false);
81
+ assert.equal(model.web_search_tool_type, "text");
82
+ assert.deepEqual(model.input_modalities, ["text"]);
83
+ assert.deepEqual(model.truncation_policy, { mode: "tokens", limit: 10000 });
84
+ assert.equal(model.auto_compact_token_limit, null);
85
+ assert.equal(model.tool_mode, null);
86
+ assert.equal(model.multi_agent_version, null);
87
+ assert.deepEqual(Object.keys(model).sort(), CODEX_MODEL_INFO_FIELDS);
88
+ });
89
+
90
+ test("does not advertise unsupported capabilities", () => {
91
+ const model = toCodexModelInfo({
92
+ alias: "provider/text-only",
93
+ provider: "provider",
94
+ model_id: "text-only",
95
+ supports_tools: false,
96
+ supports_images: false,
97
+ supports_thinking: false,
98
+ supports_web_search: false
99
+ });
100
+
101
+ assert.equal(model.default_reasoning_level, null);
102
+ assert.deepEqual(model.supported_reasoning_levels, []);
103
+ assert.equal(model.supports_reasoning_summaries, false);
104
+ assert.equal(model.apply_patch_tool_type, null);
105
+ assert.equal(model.supports_parallel_tool_calls, false);
106
+ assert.equal(model.supports_image_detail_original, false);
107
+ assert.equal(model.supports_search_tool, false);
108
+ assert.equal(model.web_search_tool_type, "text");
109
+ assert.equal(model.support_verbosity, false);
110
+ assert.equal(model.default_verbosity, null);
111
+ assert.deepEqual(model.input_modalities, ["text"]);
112
+ assert.deepEqual(Object.keys(model).sort(), CODEX_MODEL_INFO_FIELDS);
113
+ });
114
+
115
+ test("preserves explicit Codex deferred tool search capability", () => {
116
+ const model = toCodexModelInfo({
117
+ alias: "cx/gpt-5.5",
118
+ provider: "codex",
119
+ model_id: "gpt-5.5",
120
+ supports_tools: true,
121
+ supports_images: true,
122
+ supports_thinking: true,
123
+ supports_web_search: true,
124
+ supports_search_tool: true,
125
+ web_search_tool_type: "text_and_image"
126
+ });
127
+
128
+ assert.equal(model.supports_search_tool, true);
129
+ assert.equal(model.web_search_tool_type, "text_and_image");
130
+ });
131
+
132
+ test("preserves explicit disabled deferred tool search despite hosted web search", () => {
133
+ const model = toCodexModelInfo({
134
+ alias: "provider/hosted-search-only",
135
+ provider: "provider",
136
+ model_id: "hosted-search-only",
137
+ supports_tools: true,
138
+ supports_images: false,
139
+ supports_thinking: false,
140
+ supports_web_search: true,
141
+ supports_search_tool: false,
142
+ web_search_tool_type: "text"
143
+ });
144
+
145
+ assert.equal(model.supports_search_tool, false);
146
+ assert.equal(model.web_search_tool_type, "text");
147
+ });
148
+
149
+ test("strips hosted web search from runtime requests for unsupported models", () => {
150
+ const raw = JSON.stringify({
151
+ model: "deepseek/deepseek-v4-flash",
152
+ tools: [
153
+ { type: "web_search" },
154
+ { type: "function", name: "shell" }
155
+ ],
156
+ input: "hi"
157
+ });
158
+
159
+ const rewritten = JSON.parse(rewriteRuntimeModel(raw, "", {
160
+ "deepseek/deepseek-v4-flash": { supports_web_search: false }
161
+ }));
162
+
163
+ assert.equal(rewritten.model, "deepseek/deepseek-v4-flash");
164
+ assert.deepEqual(rewritten.tools, [{ type: "function", name: "shell" }]);
165
+ });
166
+
167
+ test("keeps hosted web search for supported selected models", () => {
168
+ const raw = JSON.stringify({
169
+ model: "gpt-5.5",
170
+ tools: [{ type: "web_search" }],
171
+ input: "hi"
172
+ });
173
+
174
+ const rewritten = JSON.parse(rewriteRuntimeModel(raw, "cx/gpt-5.5", {
175
+ "cx/gpt-5.5": { supports_web_search: true }
176
+ }));
177
+
178
+ assert.equal(rewritten.model, "cx/gpt-5.5");
179
+ assert.deepEqual(rewritten.tools, [{ type: "web_search" }]);
180
+ });
@@ -1,6 +1,10 @@
1
1
  import { createServer } from "node:http";
2
- import { AURORA_LOCALHOST, AURORA_SIDECAR_PORT, isRuntimePath, rewriteRuntimeModel, selectedKeyHeaders, toClientModelItem, trimSlash } from "../../protocol/src/index.js";
3
- import { readState, writeSidecarInfo } from "../../state/src/index.js";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { AURORA_LOCALHOST, AURORA_SIDECAR_PORT, isRuntimePath, rewriteRuntimeModel, selectedKeyHeaders, toClientModelItem, toCodexModelInfo, trimSlash } from "../../protocol/src/index.js";
5
+ import { CODEX_HOME, readState, writeSidecarInfo } from "../../state/src/index.js";
6
+
7
+ const CODEX_MODEL_CAPABILITIES_PATH = join(CODEX_HOME, "model_capabilities.json");
4
8
 
5
9
  export async function startSidecar(options = {}) {
6
10
  const token = options.token || readArg("--token") || process.env.AURORA_SIDECAR_TOKEN || "aurora-local-dev";
@@ -33,7 +37,7 @@ async function handle(request, response, token) {
33
37
  return;
34
38
  }
35
39
  if (request.method === "GET" && url.pathname === "/v1/models") {
36
- await proxyModels(response);
40
+ await proxyModels(response, url);
37
41
  return;
38
42
  }
39
43
  if (request.method === "POST" && isRuntimePath(url.pathname)) {
@@ -43,37 +47,78 @@ async function handle(request, response, token) {
43
47
  writeJSON(response, 404, { error: { type: "not_found", message: "unknown sidecar route" } });
44
48
  }
45
49
 
46
- async function proxyModels(response) {
50
+ async function proxyModels(response, incomingURL) {
47
51
  const state = await readState();
48
52
  const gatewayURL = trimSlash(state.gatewayURL ?? "https://auroramos.com");
49
- const upstream = await fetch(`${gatewayURL}/v1/aurora-cli/models`, {
50
- headers: selectedKeyHeaders(state)
53
+ const clientVersion = incomingURL.searchParams.get("client_version");
54
+ const upstreamURL = clientVersion
55
+ ? `${gatewayURL}/v1/aurora-cli/models?client_version=${encodeURIComponent(clientVersion)}`
56
+ : `${gatewayURL}/v1/aurora-cli/models`;
57
+ const upstream = await fetch(upstreamURL, {
58
+ headers: {
59
+ ...selectedKeyHeaders(state),
60
+ "accept-encoding": "identity"
61
+ }
51
62
  });
52
63
  const payload = await upstream.json().catch(() => ({ object: "list", data: [] }));
53
- const data = Array.isArray(payload.data) ? payload.data.map(toClientModelItem) : [];
64
+ const sourceModels = Array.isArray(payload.data) ? payload.data : [];
65
+ if (clientVersion) {
66
+ await writeCodexModelCapabilities(sourceModels);
67
+ const models = sourceModels.map((item, index) => toCodexModelInfo(item, index));
68
+ writeJSON(response, upstream.ok ? 200 : upstream.status, { models });
69
+ return;
70
+ }
71
+ const data = sourceModels.map(toClientModelItem);
54
72
  writeJSON(response, upstream.ok ? 200 : upstream.status, { object: "list", data });
55
73
  }
56
74
 
57
75
  async function proxyRuntime(request, response, path) {
58
76
  const state = await readState();
59
77
  const body = await readBody(request);
78
+ const capabilitiesByAlias = await readCodexModelCapabilities();
60
79
  const gatewayURL = trimSlash(state.gatewayURL ?? "https://auroramos.com");
61
80
  const upstream = await fetch(`${gatewayURL}${path}`, {
62
81
  method: request.method,
63
82
  headers: {
64
83
  ...copyForwardHeaders(request.headers),
65
84
  ...selectedKeyHeaders(state),
66
- "content-type": request.headers["content-type"] ?? "application/json"
85
+ "content-type": request.headers["content-type"] ?? "application/json",
86
+ "accept-encoding": "identity"
67
87
  },
68
- body: rewriteRuntimeModel(body, state.selectedModel?.alias)
88
+ body: rewriteRuntimeModel(body, state.selectedModel?.alias, capabilitiesByAlias)
69
89
  });
70
- response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries()));
90
+ response.writeHead(upstream.status, responseHeaders(upstream.headers));
71
91
  if (upstream.body) {
72
92
  for await (const chunk of upstream.body) response.write(chunk);
73
93
  }
74
94
  response.end();
75
95
  }
76
96
 
97
+ async function writeCodexModelCapabilities(sourceModels) {
98
+ const models = {};
99
+ for (const item of sourceModels) {
100
+ const alias = String(item.alias ?? item.id ?? item.model_id ?? "");
101
+ if (!alias) continue;
102
+ models[alias] = {
103
+ supports_web_search: item.supports_web_search === true,
104
+ web_search_tool_type: item.web_search_tool_type ?? null
105
+ };
106
+ }
107
+ await writeFile(CODEX_MODEL_CAPABILITIES_PATH, `${JSON.stringify({
108
+ generated_at: new Date().toISOString(),
109
+ models
110
+ }, null, 2)}\n`, { mode: 0o600 });
111
+ }
112
+
113
+ async function readCodexModelCapabilities() {
114
+ try {
115
+ const payload = JSON.parse(await readFile(CODEX_MODEL_CAPABILITIES_PATH, "utf8"));
116
+ return payload && typeof payload.models === "object" && payload.models !== null ? payload.models : {};
117
+ } catch {
118
+ return {};
119
+ }
120
+ }
121
+
77
122
  function isAuthorized(request, token) {
78
123
  const auth = String(request.headers.authorization ?? "");
79
124
  const xKey = String(request.headers["x-api-key"] ?? "");
@@ -83,12 +128,35 @@ function isAuthorized(request, token) {
83
128
  function copyForwardHeaders(headers) {
84
129
  const out = {};
85
130
  for (const [key, value] of Object.entries(headers)) {
86
- if (["host", "authorization", "x-api-key", "content-length"].includes(key.toLowerCase())) continue;
131
+ if (["host", "authorization", "x-api-key", "content-length", "accept-encoding"].includes(key.toLowerCase())) continue;
87
132
  if (value !== undefined) out[key] = Array.isArray(value) ? value.join(", ") : value;
88
133
  }
89
134
  return out;
90
135
  }
91
136
 
137
+ function responseHeaders(headers) {
138
+ const out = {};
139
+ for (const [key, value] of headers.entries()) {
140
+ const normalized = key.toLowerCase();
141
+ if ([
142
+ "connection",
143
+ "content-encoding",
144
+ "content-length",
145
+ "keep-alive",
146
+ "proxy-authenticate",
147
+ "proxy-authorization",
148
+ "te",
149
+ "trailer",
150
+ "transfer-encoding",
151
+ "upgrade"
152
+ ].includes(normalized)) {
153
+ continue;
154
+ }
155
+ out[key] = value;
156
+ }
157
+ return out;
158
+ }
159
+
92
160
  function readBody(request) {
93
161
  return new Promise((resolve, reject) => {
94
162
  const chunks = [];