codex-copilot-dx 0.8.3 → 0.8.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/README.md +35 -7
- package/bin/cli.mjs +30 -4
- package/package.json +1 -1
- package/src/adapter.mjs +68 -3
- package/src/cache-control.mjs +59 -0
- package/src/cache-limits.mjs +4 -0
- package/src/cli-cache.mjs +147 -0
- package/src/cli-image.mjs +20 -8
- package/src/cli-options.mjs +36 -1
- package/src/cli-status.mjs +11 -0
- package/src/codex-model-catalog.mjs +90 -14
- package/src/diagnostic-text.mjs +11 -0
- package/src/file-rotation.mjs +34 -0
- package/src/http-transport.mjs +11 -2
- package/src/image-mcp.mjs +17 -1
- package/src/image-optimization.mjs +12 -0
- package/src/image-provider.mjs +10 -4
- package/src/log.mjs +62 -8
- package/src/models.mjs +2 -2
- package/src/observability.mjs +11 -3
- package/src/response-failures.mjs +3 -4
- package/src/response-history.mjs +29 -2
- package/src/responses-handler.mjs +6 -5
- package/src/responses-proxy.mjs +2 -2
- package/src/running-adapter.mjs +2 -2
- package/src/stream-errors.mjs +5 -1
- package/src/usage.mjs +28 -7
- package/src/user-settings.mjs +53 -0
package/README.md
CHANGED
|
@@ -18,15 +18,19 @@ The adapter handles response history, encrypted reasoning state, function/custom
|
|
|
18
18
|
|
|
19
19
|
### Fast and Standard modes
|
|
20
20
|
|
|
21
|
-
Codex can select Standard or Fast without changing CCDX configuration. When the client requests
|
|
21
|
+
Codex can select Standard or Fast without changing CCDX configuration. When the client requests a model with `service_tier: "priority"`, CCDX uses its exact `-fast` model only when the live Copilot catalog advertises that variant as enabled, selectable, OpenAI-owned, and Responses-only. Standard mode continues to use the base model; CCDX does not invent Fast availability when the upstream catalog does not provide it. This retains the established `gpt-5.6-sol` / `gpt-5.6-sol-fast` mapping and applies the same exact-match rule to future models.
|
|
22
22
|
|
|
23
23
|
Codex Auto-review uses the hidden `codex-auto-review` model ID. CCDX maps it to Copilot's `gpt-5.5` Responses model by default, independently of the interactive Fast/Standard selection, and logs both model IDs when the mapping is used. The review target can be changed interactively without reinstalling or restarting CCDX.
|
|
24
24
|
|
|
25
25
|
### GPT-6 model catalog
|
|
26
26
|
|
|
27
|
-
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It exposes
|
|
27
|
+
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It exposes each managed GPT-6 entry only when Copilot advertises that exact model as enabled, selectable, OpenAI-owned, and Responses-capable.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
When an installed 0.155.x catalog does not yet contain `gpt-6-sol` or `gpt-6-luna`, CCDX derives their client-compatible entries from that catalog's GPT-6 Astra schema and the published OpenAI Codex model definitions at commit `0a2eb4696c26ac33204bcd255721ab30220a4774`. Only those two missing entries are inserted. Their fallback reasoning controls are intersected with Copilot's advertised efforts; every other bundled entry remains byte-for-byte equivalent. A newer client-provided Sol/Luna entry always wins and is preserved.
|
|
30
|
+
|
|
31
|
+
CCDX exposes GPT-6 Standard paths and removes unverified speed-tier metadata. It leaves bundled reasoning-effort options unchanged, including Ultra when supplied by the installed catalog. Fast follows the exact upstream-variant rule above. Versioned model discovery matches the bundled catalog to the client's `client_version`, caches it by application binary identity, and reloads it after an App update. If a matching usable catalog cannot be loaded, the versioned models request returns `503`; an unversioned request returns the Copilot-style list.
|
|
32
|
+
|
|
33
|
+
`/v1/models` still attempts live discovery first. With a usable in-process catalog, it limits that upstream wait to 1.5 seconds (or a shorter configured upstream timeout) before falling back to last-known-good data. Without a catalog it retains the regular upstream timeout. Received HTTP `401`/`403` responses remain errors, not cached successes; fallback does not grant inference access. This does not change the explicitly live `ccdx models` command.
|
|
30
34
|
|
|
31
35
|
## Prerequisites
|
|
32
36
|
|
|
@@ -78,6 +82,8 @@ Normal startup and reuse of an existing adapter check `~/.codex/config.toml` onc
|
|
|
78
82
|
|
|
79
83
|
The two token limits are fixed defaults, not detected model capabilities. Existing custom values are retained. Writes target the listed keys with format-preserving edits and atomic replacement; this is not a general TOML formatter. Quoted keys, quoted table headers, and explicit dotted shell-environment declarations are recognized; inline shell-environment tables are left intact. Startup and image configuration commands validate UTF-8/TOML and verify that unrelated settings retain their values before writing. An unsafe edit leaves the original file unchanged. Use `ccdx doctor config` to inspect parsed values and preview what this writer would do with the current file. No actual change means no file rewrite.
|
|
80
84
|
|
|
85
|
+
Interactive image setup rereads the shared config after prompts and network validation, so unrelated changes made during setup are preserved. It checks the snapshot again before committing and rejects conflicting image declarations. Identical configuration bytes are not replaced; private file permissions may still be corrected. These checks reduce lost-update risks but are not a filesystem-wide transaction with other applications that do not share a lock.
|
|
86
|
+
|
|
81
87
|
## Version 0.7.0 migration
|
|
82
88
|
|
|
83
89
|
CCDX 0.7.0 retires the former Claude App, Claude Code, Anthropic Messages API, isolated Claude account, and PM Studio patch integrations. Their setup, status, restore, profile, and routing commands are no longer supported.
|
|
@@ -111,6 +117,20 @@ The selector first queries the running adapter, including its last-known-good mo
|
|
|
111
117
|
|
|
112
118
|
The selection is saved in `~/.config/codex-copilot-dx/config.json`, or under `XDG_CONFIG_HOME` when set. Choosing `gpt-5.5` clears the override and restores the package default. A running adapter reads the setting on the next Auto-review request.
|
|
113
119
|
|
|
120
|
+
### Runtime caches
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
ccdx cache
|
|
124
|
+
ccdx cache --limit 128
|
|
125
|
+
ccdx cache --limit default
|
|
126
|
+
ccdx cache --clean
|
|
127
|
+
ccdx cache --clean --history
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`ccdx cache` separates the shared in-memory Responses history from the rebuildable input-image transform cache. The history limit accepts 16–1024 MiB, is saved in the normal CCDX settings file, and applies immediately to a compatible running adapter. `CCDX_RESPONSE_HISTORY_MAX_BYTES` remains the higher-precedence environment override. Lowering the limit below current usage is rejected rather than evicting tasks silently. `ccdx cache -128` is a short alias for `--limit 128`; `-clean` aliases `--clean`.
|
|
131
|
+
|
|
132
|
+
Plain `--clean` clears only completed image transforms; the next matching request may recompute them. Adding `--history` also invalidates all process-local `previous_response_id` chains and can make a task's next continuation fail, so it requires interactive confirmation or `--yes`. Cleanup waits for active cache users to finish and never deletes Codex transcripts, generated image files, provider credentials, image edit handles, model catalogs, or usage/debug logs. A stopped or older adapter must be restarted before runtime cleanup; a saved limit applies on the next start.
|
|
133
|
+
|
|
114
134
|
### Terminal animation
|
|
115
135
|
|
|
116
136
|
Choose the terminal activity animation interactively:
|
|
@@ -147,6 +167,8 @@ ccdx enable-image
|
|
|
147
167
|
|
|
148
168
|
The command accepts an HTTPS base URL or a full `/images/generations` endpoint, then asks for an API key without echoing the key. A bare origin such as `https://api.example` expands to `/v1/images/generations`; `https://api.example/v1` expands to the same endpoint. Custom base paths are retained, and existing full endpoints remain unchanged. CCDX uses GET for the corresponding `/models` catalog and POST for image requests; there is no endpoint-search or automatic fallback. It asks for a numeric model selection only when multiple image models are available, then detects either the standard OpenAI Images request format or the supported `input.messages` format for the selected model only. Its single validation request omits the prompt and does not generate an image.
|
|
149
169
|
|
|
170
|
+
An empty key answer retains the saved key only when the normalized endpoint has the same origin (scheme, host, and port). Changing origins requires explicitly entering a key before any provider request; the old key is never automatically sent to a new origin.
|
|
171
|
+
|
|
150
172
|
The provider credential is stored in `~/.config/codex-copilot-dx/image-provider.json`, or under `XDG_CONFIG_HOME`, with mode `0600`. The API key is never written to `~/.codex/config.toml`; that file receives only a marked local MCP entry pointing to the running CCDX adapter with a 210-second tool timeout. Only after enabling, CCDX installs `ccdx-image` guidance under `~/.codex/skills`, with a self-contained Node helper. The concise `SKILL.md` keeps operation selection, safety, and final-image delivery rules; editing details and helper instructions live in references read only for those operations. Codex uses the MCP tool directly, including through deferred tool search; an already-open task without that tool can use the helper to call the same local service and display the saved image. Neither route requires Python, an OpenAI SDK, or copying credentials. Built-in skills and unrelated tools are not modified.
|
|
151
173
|
|
|
152
174
|
The enable command verifies the local MCP handshake and generation-tool directory without generating an image. It reports provider configuration separately from local service readiness; it cannot assert that every running Codex task has refreshed its catalog. Codex normally detects changed skills; restart Codex App once if an open task has not refreshed them. Normal CCDX startup installs or updates this guidance only for an already-enabled provider, without repeating API setup. Without `enable-image`, it installs neither this skill nor the MCP registration. Unchanged guidance is not rewritten. The existing adapter can remain running because provider settings are loaded when the tool is invoked.
|
|
@@ -237,7 +259,9 @@ For the complete machine-readable status payload:
|
|
|
237
259
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
238
260
|
```
|
|
239
261
|
|
|
240
|
-
The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, model-cache counts, token expiry state, and the ten most recent upstream `response.failed` diagnostics retained by the running process. Failure diagnostics include the event type, model, error code/message, response/request IDs when supplied upstream, and retry outcome
|
|
262
|
+
The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, model-cache counts, token expiry state, and the ten most recent upstream `response.failed` diagnostics retained by the running process. Failure diagnostics include the event type, model, error code/message, response/request IDs when supplied upstream, and retry outcome. Common credential forms, long opaque encrypted values, and explicitly labelled input/prompt/tool-content echoes are redacted. They are cleared when the adapter restarts. No request/response bodies, credential fields, or account profiles are copied into status. Arbitrary upstream prose cannot be guaranteed free of sensitive content; review diagnostics before sharing them.
|
|
263
|
+
|
|
264
|
+
`image_mcp` has its own HTTP route counter. After the image handler has been used, `image_generation` separately reports active, succeeded, failed, busy, cancelled, and chat-file delivery-failure counts, including tool errors returned with HTTP 200. A delivery failure can accompany a successful generation; it never causes another generation. Before handler initialization this field is `null`, meaning not observed, not necessarily disabled. Status never initializes that handler, reads its provider credential, installs guidance, or probes the provider. Input-image optimization metrics remain separate. `logging` reports pending records/bytes, queue caps, overflow drops, and write failures for usage and enabled debug-file logging.
|
|
241
265
|
|
|
242
266
|
`stream_performance.by_route` also reports `request_ttft_ms`, measured from adapter entry to the first observed output delta, including local preparation. The existing `ttft_ms` continues to start at upstream dispatch. Neither timestamp means the client has drained or displayed the output. `preparation_ms` contains bounded histograms for admission (including history reservations), body reading/decompression/JSON parsing, history materialization/compact preparation, image optimization, and request serialization. Each sample measures one operation, including failed operations; retries can add samples. These are diagnostic intervals rather than a complete breakdown of request time: body reading includes internal decoded-body admission waits; authentication, routing, and other preparation are not individually timed. Non-streaming requests contribute preparation samples without inventing a first-token measurement.
|
|
243
267
|
|
|
@@ -269,7 +293,7 @@ Recovery first tries compatible local credentials and starts Device Flow only wh
|
|
|
269
293
|
- The GitHub OAuth credential is stored locally at `~/.local/share/copilot-api/github_token`. Short-lived Copilot service tokens remain in process memory and are refreshed as needed.
|
|
270
294
|
- The optional image API credential is stored separately with mode `0600` only after `ccdx enable-image`; it is never placed in Codex configuration or diagnostic output.
|
|
271
295
|
- Runtime status and usage records retain metadata rather than request/response content. Diagnostic logging records status and error messages; it is not a general redaction layer for arbitrary error text. See Debug logging before sharing logs.
|
|
272
|
-
- Request
|
|
296
|
+
- Request/response bodies, request-admission queues, retained history, image work, SSE buffers, log queues, retries, and shutdown have explicit limits. These component limits are not a single process-wide memory ceiling; completed image files are persistent user artifacts, not an automatically evicted cache.
|
|
273
297
|
|
|
274
298
|
## Configuration
|
|
275
299
|
|
|
@@ -316,7 +340,7 @@ Recovery first tries compatible local credentials and starts Device Flow only wh
|
|
|
316
340
|
| `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
|
|
317
341
|
| `CCDX_MODEL_REFRESH_TIMEOUT_MS` | `5000` | Timeout for Copilot model refresh or live Auto-review model lookup |
|
|
318
342
|
| `CCDX_MODEL_REFRESH_INTERVAL_MS` | `7200000` | Periodic model refresh interval; `0` disables periodic refresh, while startup cache/refresh behavior still applies |
|
|
319
|
-
| `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` |
|
|
343
|
+
| `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Higher-precedence total in-memory byte budget for locally expanded Responses history; `ccdx cache --limit` provides a persistent 16–1024 MiB setting when this is unset |
|
|
320
344
|
| `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
|
|
321
345
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token-usage log |
|
|
322
346
|
| `CCDX_USAGE_MAX_BYTES` | `33554432` | Rotate the usage log at this size and retain one `.1` backup; set to `0` to disable rotation |
|
|
@@ -333,9 +357,11 @@ ccdx usage
|
|
|
333
357
|
|
|
334
358
|
The summary covers the current log and its single rotated backup, not an unbounded lifetime history. It reports recorded responses and per-model input, cache-read, output, and total token metadata.
|
|
335
359
|
|
|
360
|
+
Usage writes are asynchronous and batched under the existing cross-process rotation lock. Pending and in-flight records share a 4096-record / 4 MiB ceiling. If either ceiling is reached, new excess records are dropped with a warning and a status counter; inference does not wait or retry. Normal records and per-record rotation boundaries are unchanged. The bounded shutdown flush remains 1.5 seconds; disk failures and shutdown deadlines can prevent persistence, so usage is diagnostic rather than a billing ledger.
|
|
361
|
+
|
|
336
362
|
## Debug logging
|
|
337
363
|
|
|
338
|
-
Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or provide a custom path. Add `CCDX_LOG_LEVEL=debug` for upstream attempts, retry causes, status codes, and timings. The adapter does not deliberately dump request/response bodies or authorization headers.
|
|
364
|
+
Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or provide a custom path. Add `CCDX_LOG_LEVEL=debug` for upstream attempts, retry causes, status codes, and timings. The adapter does not deliberately dump request/response bodies or authorization headers. File diagnostics redact common credential forms and labelled content echoes, but this cannot identify every secret in arbitrary external prose; review logs before sharing them. File writes are asynchronous and batched with a 4096-record / 2 MiB pending-and-in-flight ceiling. Overflow drops excess file records with one warning and a status counter; terminal output continues. CLI exit paths flush for at most 1.5 seconds. `ccdx doctor config` bypasses file logging entirely.
|
|
339
365
|
|
|
340
366
|
## Large histories and images
|
|
341
367
|
|
|
@@ -359,6 +385,8 @@ Changing the model or transport of a continuation can require removing incompati
|
|
|
359
385
|
|
|
360
386
|
After the stream handshake succeeds, stream liveness uses the idle timeout rather than the expired handshake deadline. Eligible HTTP-200 failure recovery acquires a bounded preparation budget and history admission before rebuilding its payload, then releases those reservations when the retry opens upstream or fails. Normal streams still release preparation admission after their response headers arrive.
|
|
361
387
|
|
|
388
|
+
Downstream backpressure waits also observe cancellation and the stream deadline. A client that stops reading cannot retain the pending stream indefinitely after timeout; CCDX closes that connection without retrying its request. Final error-frame delivery has a separate one-second maximum drain wait.
|
|
389
|
+
|
|
362
390
|
Successful streaming responses are accepted only when the upstream body is SSE and reaches its protocol terminal event. Unexpected EOF becomes a protocol-native stream error, and repeated blank tool-argument deltas are stopped per tool call. Allowlisted rate-limit, retry, model, trace, and upstream request-ID metadata are forwarded; cookies, authorization, body-length, and encoding headers are not.
|
|
363
391
|
|
|
364
392
|
CCDX keeps each native Responses message on its first upstream ID when Copilot changes that ID during streaming. Message deltas, completion events, and cached history stay consistent, addressing this cause of duplicate live messages in Codex App. Tool identities, reasoning/encrypted content, message phases, and speed-tier routing are unchanged by this ID normalization. Stable events retain their original SSE bytes; changed events are rewritten with bounded, backpressured streaming.
|
package/bin/cli.mjs
CHANGED
|
@@ -16,13 +16,14 @@ import { cliCommandName, cliHelp, parseAdapterAddressOptions, parseAdapterProbeO
|
|
|
16
16
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
17
17
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
18
18
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
19
|
-
import { autoReviewModelPreference, terminalAnimationPreference } from "../src/user-settings.mjs";
|
|
19
|
+
import { autoReviewModelPreference, responseHistoryLimitPreference, terminalAnimationPreference } from "../src/user-settings.mjs";
|
|
20
20
|
import { fetchLiveCopilotModels, formatLiveCopilotModels } from "../src/cli-models.mjs";
|
|
21
21
|
import { runAuthCommand } from "../src/cli-auth.mjs";
|
|
22
22
|
import { createProfileRuntime } from "../src/profile-runtime.mjs";
|
|
23
23
|
import { createProfileModelRuntime } from "../src/profile-model-runtime.mjs";
|
|
24
24
|
import { createCodexModelCatalog } from "../src/codex-model-catalog.mjs";
|
|
25
25
|
import { readImageProviderConfig } from "../src/image-provider-config.mjs";
|
|
26
|
+
import { setResponseHistoryMaxBytes } from "../src/response-history.mjs";
|
|
26
27
|
|
|
27
28
|
const LOCAL_VERSION = localPackageVersion();
|
|
28
29
|
const CLI_NAME = cliCommandName();
|
|
@@ -80,6 +81,26 @@ if (CLI.command === "usage") {
|
|
|
80
81
|
});
|
|
81
82
|
process.exit(0);
|
|
82
83
|
}
|
|
84
|
+
if (CLI.command === "cache") {
|
|
85
|
+
try {
|
|
86
|
+
const probe = parseAdapterProbeOptions(process.env);
|
|
87
|
+
const { runCacheCommand } = await import("../src/cli-cache.mjs");
|
|
88
|
+
await runCacheCommand({
|
|
89
|
+
action: CLI.action,
|
|
90
|
+
limitMib: CLI.limitMib,
|
|
91
|
+
resetLimit: CLI.resetLimit,
|
|
92
|
+
history: CLI.history,
|
|
93
|
+
yes: CLI.yes,
|
|
94
|
+
host: probe.adapterHost,
|
|
95
|
+
port: probe.adapterPort,
|
|
96
|
+
timeoutMs: probe.existingAdapterTimeoutMs,
|
|
97
|
+
});
|
|
98
|
+
process.exit(0);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(status("err", e.message));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
if (CLI.command === "retired") {
|
|
84
105
|
console.error(status("err", `${CLI.integration} integration was retired in ccdx 0.7.0.`));
|
|
85
106
|
if (CLI.integration === "PM Studio") {
|
|
@@ -182,6 +203,7 @@ if (CLI.command === "doctor" && CLI.configOnly) {
|
|
|
182
203
|
let RUNTIME;
|
|
183
204
|
try {
|
|
184
205
|
RUNTIME = parseRuntimeOptions(process.env);
|
|
206
|
+
setResponseHistoryMaxBytes(responseHistoryLimitPreference().bytes);
|
|
185
207
|
} catch (e) {
|
|
186
208
|
console.error(e.message);
|
|
187
209
|
process.exit(2);
|
|
@@ -248,6 +270,7 @@ if (CLI.command === "doctor") {
|
|
|
248
270
|
online: CLI.online,
|
|
249
271
|
compat: CLI.compat,
|
|
250
272
|
});
|
|
273
|
+
await LOGGING.cleanup();
|
|
251
274
|
process.exit(checks.some((check) => check.kind === "err") ? 1 : 0);
|
|
252
275
|
}
|
|
253
276
|
|
|
@@ -270,7 +293,10 @@ async function printUpdateNotice() {
|
|
|
270
293
|
try {
|
|
271
294
|
assertSafeAdapterHost(ADAPTER_HOST, process.env);
|
|
272
295
|
void runInBackground(printUpdateNotice);
|
|
273
|
-
if (await reuseRunningAdapterIfAvailable())
|
|
296
|
+
if (await reuseRunningAdapterIfAvailable()) {
|
|
297
|
+
await LOGGING.cleanup();
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
274
300
|
|
|
275
301
|
// Ensure GitHub login, using device flow if no token exists.
|
|
276
302
|
await ensureAuth();
|
|
@@ -347,7 +373,7 @@ try {
|
|
|
347
373
|
exitCode = 1;
|
|
348
374
|
console.error(status("err", `Shutdown failed: ${e.message}`));
|
|
349
375
|
} finally {
|
|
350
|
-
LOGGING.cleanup();
|
|
376
|
+
await LOGGING.cleanup();
|
|
351
377
|
process.exit(exitCode);
|
|
352
378
|
}
|
|
353
379
|
};
|
|
@@ -357,6 +383,6 @@ try {
|
|
|
357
383
|
console.error(status("err", e.message));
|
|
358
384
|
await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs }).catch(() => {});
|
|
359
385
|
await flushUsageWrites();
|
|
360
|
-
LOGGING.cleanup();
|
|
386
|
+
await LOGGING.cleanup();
|
|
361
387
|
process.exit(1);
|
|
362
388
|
}
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -17,12 +17,19 @@ import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs
|
|
|
17
17
|
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
18
18
|
import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
|
|
19
19
|
import { createResponseFailureDiagnostics } from "./response-failures.mjs";
|
|
20
|
+
import {
|
|
21
|
+
ADAPTER_CACHE_PATH,
|
|
22
|
+
cacheRuntimeSnapshot,
|
|
23
|
+
cleanRuntimeCaches,
|
|
24
|
+
setHistoryCacheLimit,
|
|
25
|
+
} from "./cache-control.mjs";
|
|
20
26
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
21
27
|
import {
|
|
22
28
|
createRequestAdmission,
|
|
23
29
|
createRequestAbort,
|
|
24
30
|
httpError,
|
|
25
31
|
logRequestFailure,
|
|
32
|
+
readJsonBody,
|
|
26
33
|
sendJsonError,
|
|
27
34
|
} from "./http-transport.mjs";
|
|
28
35
|
|
|
@@ -192,7 +199,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
192
199
|
streamIdleTimeoutMs,
|
|
193
200
|
upstreamTimeoutMs,
|
|
194
201
|
});
|
|
195
|
-
const dispatch = (req, res, pathname) => {
|
|
202
|
+
const dispatch = async (req, res, pathname) => {
|
|
196
203
|
if (pathname === "/mcp/image") {
|
|
197
204
|
if (imageMcpHandler) return imageMcpHandler(req, res);
|
|
198
205
|
return import("./image-mcp.mjs").then(({ createImageMcpHandler }) => {
|
|
@@ -213,6 +220,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
213
220
|
admission: acquireRequest,
|
|
214
221
|
imagePressure,
|
|
215
222
|
responseFailures,
|
|
223
|
+
imageGeneration: imageMcpHandler?.stats?.(),
|
|
216
224
|
modelRegistry: codexModelRegistry,
|
|
217
225
|
codexClient,
|
|
218
226
|
codexModelRegistry,
|
|
@@ -226,6 +234,56 @@ export function createAdapterHandler(options = {}) {
|
|
|
226
234
|
return;
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
if (pathname === ADAPTER_CACHE_PATH) {
|
|
238
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
239
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
240
|
+
res.end(JSON.stringify({ error: "Cache control is available only from loopback" }));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (req.method === "GET") {
|
|
244
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
245
|
+
res.end(JSON.stringify(cacheRuntimeSnapshot()));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (req.method !== "POST") {
|
|
249
|
+
res.writeHead(405, { "Content-Type": "application/json", Allow: "GET, POST" });
|
|
250
|
+
res.end(JSON.stringify({ error: "Only GET and POST are supported" }));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (Object.hasOwn(req.headers || {}, "origin")) {
|
|
254
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
255
|
+
res.end(JSON.stringify({ error: "Browser origins are not allowed for cache control" }));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const mediaType = String(req.headers?.["content-type"] || "").split(";", 1)[0].trim().toLowerCase();
|
|
259
|
+
if (mediaType !== "application/json") {
|
|
260
|
+
res.writeHead(415, { "Content-Type": "application/json" });
|
|
261
|
+
res.end(JSON.stringify({ error: "Cache control requires application/json" }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const abort = createRequestAbort(req, res);
|
|
265
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
266
|
+
try {
|
|
267
|
+
const body = await readJsonBody(req, {
|
|
268
|
+
maxBodyBytes: 8 * 1024,
|
|
269
|
+
maxDecodedBodyBytes: 8 * 1024,
|
|
270
|
+
signal: abort.signal,
|
|
271
|
+
});
|
|
272
|
+
let result;
|
|
273
|
+
if (body?.action === "set_limit") result = setHistoryCacheLimit(body.max_bytes);
|
|
274
|
+
else if (body?.action === "clean") result = cleanRuntimeCaches({ history: body.history === true });
|
|
275
|
+
else throw httpError("Unknown cache action", 400);
|
|
276
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
277
|
+
res.end(JSON.stringify(result));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (["ccdx_cache_busy", "ccdx_cache_limit_below_usage"].includes(error?.code)) error.statusCode = 409;
|
|
280
|
+
sendJsonError(res, error, 400);
|
|
281
|
+
} finally {
|
|
282
|
+
abort.cleanup();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
229
287
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
230
288
|
return responsesHandler(req, res);
|
|
231
289
|
}
|
|
@@ -237,7 +295,12 @@ export function createAdapterHandler(options = {}) {
|
|
|
237
295
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
238
296
|
const clientVersion = modelClientVersion(req.url);
|
|
239
297
|
const abort = createRequestAbort(req, res);
|
|
240
|
-
|
|
298
|
+
// Keep live discovery and explicit auth failures authoritative, but do
|
|
299
|
+
// not make a usable cached picker wait the full inference timeout.
|
|
300
|
+
const modelTimeoutMs = isValidModelList(codexModelRegistry?.models)
|
|
301
|
+
? Math.min(upstreamTimeoutMs, parsePositiveInteger(options.cachedModelsTimeoutMs, 1500))
|
|
302
|
+
: upstreamTimeoutMs;
|
|
303
|
+
abort.setTimeout(modelTimeoutMs);
|
|
241
304
|
listModelsFn({ signal: abort.signal })
|
|
242
305
|
.then(async ({ status, body }) => {
|
|
243
306
|
abort.clearTimeout();
|
|
@@ -308,7 +371,9 @@ export function createAdapterHandler(options = {}) {
|
|
|
308
371
|
return;
|
|
309
372
|
}
|
|
310
373
|
|
|
311
|
-
const trackRequest = pathname !== ADAPTER_HEALTH_PATH
|
|
374
|
+
const trackRequest = pathname !== ADAPTER_HEALTH_PATH
|
|
375
|
+
&& pathname !== ADAPTER_STATUS_PATH
|
|
376
|
+
&& pathname !== ADAPTER_CACHE_PATH;
|
|
312
377
|
const routeName = classifyAdapterRoute(req.method, pathname);
|
|
313
378
|
const complete = trackRequest ? requestMetrics.begin(routeName) : () => {};
|
|
314
379
|
const streamPerformance = trackRequest ? streamPerformanceMetrics.begin(routeName) : null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MIB,
|
|
3
|
+
RESPONSE_HISTORY_MAX_MIB,
|
|
4
|
+
RESPONSE_HISTORY_MIN_MIB,
|
|
5
|
+
} from "./cache-limits.mjs";
|
|
6
|
+
import {
|
|
7
|
+
clearResponseHistory,
|
|
8
|
+
responseHistoryStats,
|
|
9
|
+
setResponseHistoryMaxBytes,
|
|
10
|
+
} from "./response-history.mjs";
|
|
11
|
+
import {
|
|
12
|
+
clearImageOptimizationCache,
|
|
13
|
+
imageOptimizationStats,
|
|
14
|
+
} from "./image-optimization.mjs";
|
|
15
|
+
|
|
16
|
+
export const ADAPTER_CACHE_PATH = "/_ccdx/cache";
|
|
17
|
+
|
|
18
|
+
export function cacheRuntimeSnapshot() {
|
|
19
|
+
return {
|
|
20
|
+
response_history: responseHistoryStats(),
|
|
21
|
+
image_optimization: imageOptimizationStats(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setHistoryCacheLimit(maxBytes) {
|
|
26
|
+
const mib = Number(maxBytes) / MIB;
|
|
27
|
+
if (!Number.isSafeInteger(mib)
|
|
28
|
+
|| mib < RESPONSE_HISTORY_MIN_MIB
|
|
29
|
+
|| mib > RESPONSE_HISTORY_MAX_MIB) {
|
|
30
|
+
const error = new Error(`History cache limit must be an integer from ${RESPONSE_HISTORY_MIN_MIB} to ${RESPONSE_HISTORY_MAX_MIB} MiB`);
|
|
31
|
+
error.code = "ccdx_cache_limit_invalid";
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return { changed: setResponseHistoryMaxBytes(maxBytes).previousMaxBytes !== maxBytes, ...cacheRuntimeSnapshot() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function cleanRuntimeCaches({ history = false } = {}) {
|
|
38
|
+
const imageStats = imageOptimizationStats();
|
|
39
|
+
if (imageStats.active > 0 || imageStats.queued > 0 || imageStats.cache_inflight > 0) {
|
|
40
|
+
const error = new Error("Image optimization is active; retry cache cleanup after current requests finish");
|
|
41
|
+
error.code = "ccdx_cache_busy";
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const historyStats = responseHistoryStats();
|
|
45
|
+
if (history && historyStats.pinnedTrees > 0) {
|
|
46
|
+
const error = new Error("Response history is active; retry cleanup after current requests finish");
|
|
47
|
+
error.code = "ccdx_cache_busy";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const imageOptimization = clearImageOptimizationCache();
|
|
51
|
+
const responseHistory = history ? clearResponseHistory() : null;
|
|
52
|
+
return {
|
|
53
|
+
cleaned: {
|
|
54
|
+
image_optimization: imageOptimization,
|
|
55
|
+
...(responseHistory ? { response_history: responseHistory } : {}),
|
|
56
|
+
},
|
|
57
|
+
...cacheRuntimeSnapshot(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { ADAPTER_CACHE_PATH } from "./cache-control.mjs";
|
|
4
|
+
import { MIB, RESPONSE_HISTORY_DEFAULT_MIB } from "./cache-limits.mjs";
|
|
5
|
+
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
6
|
+
import {
|
|
7
|
+
readUserSettings,
|
|
8
|
+
responseHistoryLimitPreference,
|
|
9
|
+
writeResponseHistoryLimitMib,
|
|
10
|
+
} from "./user-settings.mjs";
|
|
11
|
+
|
|
12
|
+
const RESPONSE_HISTORY_MAX_MIB_KEY = "response_history_max_mib";
|
|
13
|
+
|
|
14
|
+
function mib(bytes) {
|
|
15
|
+
const value = Number(bytes);
|
|
16
|
+
return Number.isFinite(value) ? `${(value / MIB).toFixed(value % MIB === 0 ? 0 : 1)} MiB` : "unknown";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function cacheRequest(baseUrl, { body, fetchImpl, timeoutMs }) {
|
|
20
|
+
const response = await fetchImpl(`${baseUrl}${ADAPTER_CACHE_PATH}`, {
|
|
21
|
+
method: body ? "POST" : "GET",
|
|
22
|
+
headers: { Accept: "application/json", ...(body ? { "Content-Type": "application/json" } : {}) },
|
|
23
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
24
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
25
|
+
});
|
|
26
|
+
let payload;
|
|
27
|
+
try { payload = await response.json(); } catch { payload = null; }
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const error = new Error(payload?.error || `Cache control returned HTTP ${response.status}`);
|
|
30
|
+
error.statusCode = response.status;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runningSnapshot(baseUrl, options) {
|
|
37
|
+
try { return { supported: true, data: await cacheRequest(baseUrl, options) }; }
|
|
38
|
+
catch (error) {
|
|
39
|
+
if ([404, 405].includes(error?.statusCode)) return { supported: false, error };
|
|
40
|
+
if (error?.name === "TimeoutError" || error?.cause?.code === "ECONNREFUSED") return { supported: false, offline: true, error };
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function confirmHistoryCleanup({ input, output, prompt, yes }) {
|
|
46
|
+
if (yes) return;
|
|
47
|
+
if (prompt) {
|
|
48
|
+
if (!await prompt()) throw new Error("History cache cleanup cancelled");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!input.isTTY || !output.isTTY) {
|
|
52
|
+
throw new Error("History cleanup can break task continuation; rerun with --yes to confirm");
|
|
53
|
+
}
|
|
54
|
+
const readline = createInterface({ input, output });
|
|
55
|
+
try {
|
|
56
|
+
const answer = String(await readline.question(
|
|
57
|
+
"Clear response history for all tasks? Their next continuation may fail [y/N]: ",
|
|
58
|
+
)).trim().toLowerCase();
|
|
59
|
+
if (!["y", "yes"].includes(answer)) throw new Error("History cache cleanup cancelled");
|
|
60
|
+
} finally { readline.close(); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatStatus(preference, runtime) {
|
|
64
|
+
const lines = ["ccdx cache", `Configured history limit: ${mib(preference.bytes)} (${preference.source})`];
|
|
65
|
+
if (!runtime?.supported) {
|
|
66
|
+
lines.push(runtime?.offline
|
|
67
|
+
? "Runtime cache: adapter is not running"
|
|
68
|
+
: "Runtime cache: restart ccdx to enable cache control");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
const history = runtime.data.response_history || {};
|
|
72
|
+
const images = runtime.data.image_optimization || {};
|
|
73
|
+
lines.push(
|
|
74
|
+
`History: ${mib(history.bytes)} / ${mib(history.maxBytes)}, ${history.entries || 0} entries, ${history.evicted || 0} evicted`,
|
|
75
|
+
`Image transforms: ${mib(images.cache_bytes)} / ${mib(images.cache_max_bytes)}, ${images.cache_entries || 0} entries`,
|
|
76
|
+
);
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runCacheCommand({
|
|
81
|
+
action = "status",
|
|
82
|
+
limitMib,
|
|
83
|
+
resetLimit = false,
|
|
84
|
+
history = false,
|
|
85
|
+
yes = false,
|
|
86
|
+
env = process.env,
|
|
87
|
+
home = os.homedir(),
|
|
88
|
+
host = "127.0.0.1",
|
|
89
|
+
port = 2026,
|
|
90
|
+
input = process.stdin,
|
|
91
|
+
output = process.stdout,
|
|
92
|
+
prompt,
|
|
93
|
+
fetchImpl = fetch,
|
|
94
|
+
timeoutMs = 1000,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const baseUrl = adapterBaseUrl(host, port);
|
|
97
|
+
const requestOptions = { fetchImpl, timeoutMs };
|
|
98
|
+
const preference = responseHistoryLimitPreference({ env, home });
|
|
99
|
+
const runtime = await runningSnapshot(baseUrl, requestOptions);
|
|
100
|
+
if (action === "status") {
|
|
101
|
+
const text = formatStatus(preference, runtime);
|
|
102
|
+
output.write(`${text}\n`);
|
|
103
|
+
return { preference, runtime };
|
|
104
|
+
}
|
|
105
|
+
if (action === "limit") {
|
|
106
|
+
if (String(env.CCDX_RESPONSE_HISTORY_MAX_BYTES || "").trim()) {
|
|
107
|
+
throw new Error("CCDX_RESPONSE_HISTORY_MAX_BYTES overrides saved cache settings; unset it before using ccdx cache --limit");
|
|
108
|
+
}
|
|
109
|
+
const targetMib = resetLimit ? RESPONSE_HISTORY_DEFAULT_MIB : Number(limitMib);
|
|
110
|
+
const targetBytes = targetMib * MIB;
|
|
111
|
+
if (runtime.supported && targetBytes < runtime.data.response_history.bytes) {
|
|
112
|
+
throw new Error(`History currently uses ${mib(runtime.data.response_history.bytes)}; clean history or choose a larger limit`);
|
|
113
|
+
}
|
|
114
|
+
const previous = readUserSettings({ env, home, strict: true });
|
|
115
|
+
const previousMib = Object.hasOwn(previous, RESPONSE_HISTORY_MAX_MIB_KEY)
|
|
116
|
+
? previous[RESPONSE_HISTORY_MAX_MIB_KEY]
|
|
117
|
+
: null;
|
|
118
|
+
const saved = writeResponseHistoryLimitMib(resetLimit ? null : targetMib, { env, home });
|
|
119
|
+
try {
|
|
120
|
+
if (runtime.supported) {
|
|
121
|
+
await cacheRequest(baseUrl, { ...requestOptions, body: { action: "set_limit", max_bytes: targetBytes } });
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
writeResponseHistoryLimitMib(previousMib, { env, home });
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
output.write(`History cache limit: ${targetMib} MiB${runtime.supported ? " (active now)" : " (saved; restart ccdx to apply)"}\n`);
|
|
128
|
+
return { ...saved, active: runtime.supported };
|
|
129
|
+
}
|
|
130
|
+
if (action === "clean") {
|
|
131
|
+
if (!runtime.supported) throw new Error("Start or restart ccdx before cleaning runtime caches");
|
|
132
|
+
if (history) await confirmHistoryCleanup({ input, output, prompt, yes });
|
|
133
|
+
const result = await cacheRequest(baseUrl, {
|
|
134
|
+
...requestOptions,
|
|
135
|
+
body: { action: "clean", history },
|
|
136
|
+
});
|
|
137
|
+
const images = result.cleaned.image_optimization;
|
|
138
|
+
output.write(`Cleared image transform cache: ${images.entries} entries, ${mib(images.bytes)}\n`);
|
|
139
|
+
if (history) {
|
|
140
|
+
const cleared = result.cleaned.response_history;
|
|
141
|
+
output.write(`Cleared response history: ${cleared.entries} entries, ${mib(cleared.bytes)}\n`);
|
|
142
|
+
output.write("Saved Codex transcripts and generated image files were not deleted.\n");
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unknown cache action: ${action}`);
|
|
147
|
+
}
|
package/src/cli-image.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { atomicWriteFilePairSync } from "./atomic-file.mjs";
|
|
6
|
+
import { atomicWriteFileIfChangedSync, atomicWriteFilePairSync } from "./atomic-file.mjs";
|
|
7
7
|
import { terminalCell } from "./cli-table.mjs";
|
|
8
8
|
import { computeImageMcpCodexConfig } from "./config.mjs";
|
|
9
9
|
import { probeImageTool } from "./image-readiness.mjs";
|
|
@@ -11,6 +11,7 @@ import { updateImageSkill } from "./image-skill.mjs";
|
|
|
11
11
|
import {
|
|
12
12
|
imageProviderConfigPath,
|
|
13
13
|
inspectImageProvider,
|
|
14
|
+
normalizeImageEndpoint,
|
|
14
15
|
readImageProviderConfig,
|
|
15
16
|
validateImageProviderConfig,
|
|
16
17
|
} from "./image-provider-config.mjs";
|
|
@@ -110,12 +111,15 @@ function enableImageProvider({ providerConfig, providerPath, codexPath, codexCon
|
|
|
110
111
|
const normalized = validateImageProviderConfig(providerConfig, { filePath: providerPath });
|
|
111
112
|
const skill = updateImageSkill({ enabled: true, codexPath, adapterPort, adapterHost });
|
|
112
113
|
try {
|
|
114
|
+
if (fileContent(codexPath) !== codexContent) {
|
|
115
|
+
throw new Error("Codex configuration changed during image setup; retry to preserve the newer settings");
|
|
116
|
+
}
|
|
113
117
|
atomicWriteFilePairSync(
|
|
114
118
|
codexPath,
|
|
115
119
|
codex.content,
|
|
116
120
|
providerPath,
|
|
117
121
|
`${JSON.stringify(normalized, null, 2)}\n`,
|
|
118
|
-
{ mode: 0o600 },
|
|
122
|
+
{ mode: 0o600, writeFile: atomicWriteFileIfChangedSync },
|
|
119
123
|
);
|
|
120
124
|
} catch (error) {
|
|
121
125
|
skill.rollback();
|
|
@@ -134,7 +138,12 @@ function disableImageProvider({ providerPath, codexPath, codexContent, adapterPo
|
|
|
134
138
|
const skill = updateImageSkill({ enabled: false, codexPath, adapterPort, adapterHost });
|
|
135
139
|
try {
|
|
136
140
|
if (fs.existsSync(codexPath)) {
|
|
137
|
-
|
|
141
|
+
if (fileContent(codexPath) !== codexContent) {
|
|
142
|
+
throw new Error("Codex configuration changed during image cleanup; retry to preserve the newer settings");
|
|
143
|
+
}
|
|
144
|
+
atomicWriteFilePairSync(codexPath, codex.content, providerPath, null, {
|
|
145
|
+
mode: 0o600, writeFile: atomicWriteFileIfChangedSync,
|
|
146
|
+
});
|
|
138
147
|
} else if (providerExists) {
|
|
139
148
|
fs.unlinkSync(providerPath);
|
|
140
149
|
}
|
|
@@ -203,10 +212,11 @@ export async function runImageCommand({
|
|
|
203
212
|
output.write(`${commandName} enable-image\n`);
|
|
204
213
|
output.write("Enter an HTTPS base URL (for example, https://api.example/v1) or a full /images/generations endpoint.\n");
|
|
205
214
|
const endpointAnswer = String(await ask(`API base URL or endpoint${current ? ` [${current.endpoint}]` : ""}: `) || "").trim();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const
|
|
215
|
+
if (!endpointAnswer && !current?.endpoint) throw new Error("Image API base URL or endpoint is required");
|
|
216
|
+
const endpoint = normalizeImageEndpoint(endpointAnswer || current.endpoint);
|
|
217
|
+
const sameOrigin = current && new URL(endpoint).origin === new URL(current.endpoint).origin;
|
|
218
|
+
const keyAnswer = String(await askSecret(`API key${sameOrigin ? " [Enter to keep current]" : current ? " [new origin; enter a key]" : ""}: `) || "").trim();
|
|
219
|
+
const apiKey = keyAnswer || (sameOrigin ? current.api_key : undefined);
|
|
210
220
|
if (!apiKey) throw new Error("Image API key is required");
|
|
211
221
|
|
|
212
222
|
output.write("Checking image API...\n");
|
|
@@ -227,7 +237,9 @@ export async function runImageCommand({
|
|
|
227
237
|
},
|
|
228
238
|
providerPath,
|
|
229
239
|
codexPath,
|
|
230
|
-
|
|
240
|
+
// Prompts and provider validation may take minutes. Merge into the latest
|
|
241
|
+
// shared file rather than replacing edits made while we were waiting.
|
|
242
|
+
codexContent: fileContent(codexPath),
|
|
231
243
|
adapterPort,
|
|
232
244
|
adapterHost,
|
|
233
245
|
});
|