codex-copilot-dx 0.6.15 → 0.6.19
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 +28 -15
- package/bin/cli.mjs +7 -1
- package/package.json +1 -1
- package/src/adapter.mjs +47 -10
- package/src/anthropic-handler.mjs +25 -6
- package/src/anthropic.mjs +19 -7
- package/src/atomic-file.mjs +76 -2
- package/src/auth-profile.mjs +11 -34
- package/src/auth.mjs +265 -52
- package/src/cli-auth.mjs +124 -94
- package/src/cli-error.mjs +44 -0
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +9 -2
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +138 -0
- package/src/lock.mjs +199 -17
- package/src/models.mjs +17 -0
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +26 -5
- package/src/pm-studio-setup.mjs +351 -16
- package/src/pm-studio-status.mjs +5 -1
- package/src/response-history.mjs +122 -13
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +123 -36
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +13 -1
- package/src/responses-request.mjs +17 -5
- package/src/runtime-config.mjs +10 -0
- package/src/token-counter-service.mjs +188 -0
- package/src/token-counter-worker.mjs +10 -0
- package/src/usage.mjs +25 -7
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { terminalCell } from "./cli-table.mjs";
|
|
2
|
+
import { status } from "./status.mjs";
|
|
3
|
+
|
|
4
|
+
const MAX_MESSAGE_CHARACTERS = 1024;
|
|
5
|
+
const MAX_DETAIL_CHARACTERS = 512;
|
|
6
|
+
const MAX_DETAILS = 16;
|
|
7
|
+
|
|
8
|
+
function redactSensitiveValues(value) {
|
|
9
|
+
return value
|
|
10
|
+
.replace(/\b(authorization\s*[:=]\s*)(?:bearer\s+)?[^\s,;]+/gi, "$1[REDACTED]")
|
|
11
|
+
.replace(/\bbearer\s+[^\s,;]+/gi, "Bearer [REDACTED]")
|
|
12
|
+
.replace(/\b((?:api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|password|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[REDACTED]")
|
|
13
|
+
.replace(/\b(?:github_pat_[A-Za-z0-9_]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}|sk-[A-Za-z0-9_-]{8,})\b/g, "[REDACTED]");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function boundedTerminalText(value, fallback, maxCharacters) {
|
|
17
|
+
try {
|
|
18
|
+
return redactSensitiveValues(terminalCell(value, { fallback })).slice(0, maxCharacters);
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatCliError(error) {
|
|
25
|
+
const message = boundedTerminalText(error?.message, "Command failed", MAX_MESSAGE_CHARACTERS);
|
|
26
|
+
let details;
|
|
27
|
+
try {
|
|
28
|
+
details = error?.details;
|
|
29
|
+
} catch {
|
|
30
|
+
details = undefined;
|
|
31
|
+
}
|
|
32
|
+
const values = details === undefined
|
|
33
|
+
? []
|
|
34
|
+
: Array.isArray(details) ? details : [details];
|
|
35
|
+
const visible = values.slice(0, MAX_DETAILS);
|
|
36
|
+
const lines = [status("err", message)];
|
|
37
|
+
for (const detail of visible) {
|
|
38
|
+
lines.push(status("info", boundedTerminalText(detail, "Detail unavailable", MAX_DETAIL_CHARACTERS)));
|
|
39
|
+
}
|
|
40
|
+
if (values.length > visible.length) {
|
|
41
|
+
lines.push(status("info", `${values.length - visible.length} additional detail(s) omitted`));
|
|
42
|
+
}
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const HELP_TOPICS = new Set([
|
|
|
18
18
|
"pms",
|
|
19
19
|
"pms setup",
|
|
20
20
|
"pms status",
|
|
21
|
+
"pms restore",
|
|
21
22
|
"usage",
|
|
22
23
|
"auto-review-model",
|
|
23
24
|
"update",
|
|
@@ -154,9 +155,9 @@ export function parseCliArgs(args = []) {
|
|
|
154
155
|
}
|
|
155
156
|
if (PM_COMMANDS.has(command)) {
|
|
156
157
|
const [action, ...pmsArgs] = rest;
|
|
157
|
-
if (!action) throw new Error("Missing pms action: expected setup or
|
|
158
|
-
if (!new Set(["setup", "status"]).has(action)) throw new Error(`Unknown pms action: ${action}`);
|
|
159
|
-
if (action === "setup") {
|
|
158
|
+
if (!action) throw new Error("Missing pms action: expected setup, status, or restore");
|
|
159
|
+
if (!new Set(["setup", "status", "restore"]).has(action)) throw new Error(`Unknown pms action: ${action}`);
|
|
160
|
+
if (action === "setup" || action === "restore") {
|
|
160
161
|
if (pmsArgs.length) unexpectedArgs(pmsArgs);
|
|
161
162
|
return baseCommand("pms", { action });
|
|
162
163
|
}
|
|
@@ -292,9 +293,10 @@ function topicHelp(name, topic) {
|
|
|
292
293
|
doctor: `Usage:\n ${name} doctor [--online] [--compat] [--profile codex|claude|all]\n\nChecks local credentials, client configuration, PM Studio state, and the adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
|
|
293
294
|
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, cache, queue, and PM relay metrics from a running adapter.`,
|
|
294
295
|
models: `Usage:\n ${name} models [--profile codex|claude] [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for one saved profile. Interactive terminals use a table by default.`,
|
|
295
|
-
pms: `Usage:\n ${name} pms status [--format table|plain]\n ${name} pms setup\n ${name} pm-studio status [--format table|plain]\n ${name} pm-studio setup\n\nInspects or
|
|
296
|
+
pms: `Usage:\n ${name} pms status [--format table|plain]\n ${name} pms setup\n ${name} pms restore\n ${name} pm-studio status [--format table|plain]\n ${name} pm-studio setup\n ${name} pm-studio restore\n\nInspects, installs, or restores the fail-closed PM Studio split-origin patch. setup and restore modify the installed app only after source-bound compatibility and backup checks pass.`,
|
|
296
297
|
"pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an exact or structurally compatible PM Studio bundle. Unverifiable versions and drift are never modified.`,
|
|
297
298
|
"pms status": `Usage:\n ${name} pms status [--format table|plain]\n\nRead-only inspection of the installed PM Studio version, patch integrity, Claude profile, and relay availability. Interactive terminals use a table by default.`,
|
|
299
|
+
"pms restore": `Usage:\n ${name} pms restore\n\nRestores the installed PM Studio bundle from its exact source-bound verified backup. The explicit command authorizes the change; no confirmation prompt is shown. Missing, ambiguous, version-mismatched, or drifted records are never restored.`,
|
|
298
300
|
usage: `Usage:\n ${name} usage [--format table|plain]\n\nSummarizes local token usage metadata without reading prompt or completion content. Interactive terminals use a table by default.`,
|
|
299
301
|
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an enabled Responses model for Codex Auto-review.`,
|
|
300
302
|
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
|
|
@@ -316,6 +318,7 @@ export function cliHelp(commandName = "ccdx", topic = "") {
|
|
|
316
318
|
${name} models [--profile codex|claude] [--format table|plain]
|
|
317
319
|
${name} pms status [--format table|plain]
|
|
318
320
|
${name} pms setup
|
|
321
|
+
${name} pms restore
|
|
319
322
|
${name} usage [--format table|plain]
|
|
320
323
|
${name} auto-review-model
|
|
321
324
|
${name} update [npm|github]
|
|
@@ -328,7 +331,7 @@ Commands:
|
|
|
328
331
|
doctor Diagnose local config; optional live and inference checks
|
|
329
332
|
status Show runtime routing, performance, queue, and cache health
|
|
330
333
|
models Query a saved account's live Copilot model catalog
|
|
331
|
-
pms, pm-studio Inspect or
|
|
334
|
+
pms, pm-studio Inspect, install, or restore the PM Studio patch
|
|
332
335
|
usage Summarize locally recorded token usage
|
|
333
336
|
auto-review-model Select the Codex Auto-review Responses model
|
|
334
337
|
update Update the global package from npm or GitHub
|
package/src/copilot-client.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCopilotTokenSession } from "./copilot-token-session.mjs";
|
|
2
|
+
import { MAX_UPSTREAM_MODEL_CATALOG_BYTES, readBoundedResponseText } from "./http-transport.mjs";
|
|
2
3
|
import { prepareResponsesPayload, summarizeReqBody } from "./image-optimization.mjs";
|
|
3
4
|
import { debugLog } from "./log.mjs";
|
|
4
5
|
import { enforceResponsesPayloadByteBudget } from "./responses-byte-budget.mjs";
|
|
@@ -117,7 +118,10 @@ export function createCopilotClientRuntime({
|
|
|
117
118
|
headers,
|
|
118
119
|
signal,
|
|
119
120
|
}, { fetchImpl, ...retryOptions });
|
|
120
|
-
const body = await response
|
|
121
|
+
const body = await readBoundedResponseText(response, {
|
|
122
|
+
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
123
|
+
label: "Copilot model catalog",
|
|
124
|
+
});
|
|
121
125
|
if (response.ok) {
|
|
122
126
|
try { cacheModelEndpoints(JSON.parse(body)); } catch {}
|
|
123
127
|
}
|
|
@@ -4,7 +4,11 @@ import {
|
|
|
4
4
|
isImageNamespaceCollisionError,
|
|
5
5
|
sanitizeImageNamespaceCollisionRequest,
|
|
6
6
|
} from "./copilot-responses-policy.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
httpError,
|
|
9
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
10
|
+
readBoundedResponseText,
|
|
11
|
+
} from "./http-transport.mjs";
|
|
8
12
|
import {
|
|
9
13
|
finalizeEncryptedHistoryRebase,
|
|
10
14
|
sanitizeEncryptedReasoningRequest,
|
|
@@ -48,7 +52,10 @@ export async function openCopilotResponse(reqContext, upstream = copilotResponse
|
|
|
48
52
|
return { resp, reqContext };
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
const errorText = await resp
|
|
55
|
+
const errorText = await readBoundedResponseText(resp, {
|
|
56
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
57
|
+
label: "Copilot Responses error body",
|
|
58
|
+
});
|
|
52
59
|
options.assertPrepareActive?.();
|
|
53
60
|
let retryPolicy = null;
|
|
54
61
|
let retryContext = null;
|
package/src/copilot.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { status } from "./status.mjs";
|
|
3
3
|
import { debugLog } from "./log.mjs";
|
|
4
4
|
import { createCopilotClientRuntime } from "./copilot-client.mjs";
|
|
5
|
+
import { discardBoundedResponseBody } from "./http-transport.mjs";
|
|
5
6
|
|
|
6
7
|
export {
|
|
7
8
|
optimizeImageDataUrl,
|
|
@@ -156,7 +157,7 @@ export async function fetchCopilotUpstream(
|
|
|
156
157
|
const resp = await fetchImpl(url, init);
|
|
157
158
|
debugLog(`upstream ${method} ${target} status=${resp.status} attempt=${attempt + 1}/${retryCount + 1} attempt_ms=${Date.now() - attemptStart} total_ms=${Date.now() - totalStart}`);
|
|
158
159
|
if (attempt < retryCount && isRetryableUpstreamStatus(resp.status, method)) {
|
|
159
|
-
await resp
|
|
160
|
+
await discardBoundedResponseBody(resp);
|
|
160
161
|
const delay = upstreamRetryDelay(attempt, baseDelay);
|
|
161
162
|
console.warn(status("warn", `upstream ${target} returned ${resp.status}; retry ${attempt + 1}/${retryCount} in ${delay}ms`));
|
|
162
163
|
await sleep(delay, { signal });
|
package/src/http-transport.mjs
CHANGED
|
@@ -17,12 +17,150 @@ const MAX_INFLIGHT_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxInflightBodyBytes;
|
|
|
17
17
|
const MAX_QUEUED_REQUESTS = HTTP_RUNTIME_CONFIG.maxQueuedRequests;
|
|
18
18
|
const REQUEST_QUEUE_TIMEOUT_MS = HTTP_RUNTIME_CONFIG.requestQueueTimeoutMs;
|
|
19
19
|
|
|
20
|
+
export const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
|
|
21
|
+
export const MAX_UPSTREAM_MODEL_CATALOG_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
export const MAX_UPSTREAM_RETRY_DRAIN_BYTES = 64 * 1024;
|
|
23
|
+
export const MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxUpstreamChatResponseBytes;
|
|
24
|
+
export const MAX_UPSTREAM_RESPONSES_SUCCESS_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxUpstreamResponsesResponseBytes;
|
|
25
|
+
|
|
20
26
|
export function httpError(message, statusCode) {
|
|
21
27
|
const err = new Error(message);
|
|
22
28
|
err.statusCode = statusCode;
|
|
23
29
|
return err;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
function upstreamBodyTooLarge(label, maxBytes) {
|
|
33
|
+
const error = httpError(`${label} exceeds ${maxBytes} bytes`, 502);
|
|
34
|
+
error.code = "ccdx_upstream_response_too_large";
|
|
35
|
+
error.jsonBody = {
|
|
36
|
+
error: {
|
|
37
|
+
message: error.message,
|
|
38
|
+
type: "upstream_error",
|
|
39
|
+
code: error.code,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function unreadableUpstreamBody(label) {
|
|
46
|
+
const error = httpError(`${label} is not a readable web stream`, 502);
|
|
47
|
+
error.code = "ccdx_upstream_response_unreadable";
|
|
48
|
+
error.jsonBody = {
|
|
49
|
+
error: {
|
|
50
|
+
message: error.message,
|
|
51
|
+
type: "upstream_error",
|
|
52
|
+
code: error.code,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function cancelResponseBody(response) {
|
|
59
|
+
try { await response?.body?.cancel?.(); } catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function responseAbortError(signal) {
|
|
63
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
64
|
+
const error = new Error("The operation was aborted");
|
|
65
|
+
error.name = "AbortError";
|
|
66
|
+
return error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readResponseChunk(reader, signal) {
|
|
70
|
+
if (!signal) return reader.read();
|
|
71
|
+
if (signal.aborted) return Promise.reject(responseAbortError(signal));
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let settled = false;
|
|
74
|
+
const finish = (callback, value) => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
signal.removeEventListener("abort", onAbort);
|
|
78
|
+
callback(value);
|
|
79
|
+
};
|
|
80
|
+
const onAbort = () => {
|
|
81
|
+
const error = responseAbortError(signal);
|
|
82
|
+
try { Promise.resolve(reader.cancel(error)).catch(() => {}); } catch {}
|
|
83
|
+
finish(reject, error);
|
|
84
|
+
};
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
reader.read().then(
|
|
87
|
+
(value) => finish(resolve, value),
|
|
88
|
+
(error) => finish(reject, error),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function responseContentLength(response) {
|
|
94
|
+
const raw = response?.headers?.get?.("content-length")
|
|
95
|
+
?? response?.headers?.["content-length"]
|
|
96
|
+
?? response?.headers?.["Content-Length"];
|
|
97
|
+
const parsed = Number.parseInt(raw || "", 10);
|
|
98
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function readBoundedResponseBuffer(response, {
|
|
102
|
+
maxBytes,
|
|
103
|
+
label = "Upstream response body",
|
|
104
|
+
signal,
|
|
105
|
+
} = {}) {
|
|
106
|
+
const limit = Number.isFinite(maxBytes) && maxBytes >= 0 ? Math.floor(maxBytes) : 0;
|
|
107
|
+
if (signal?.aborted) {
|
|
108
|
+
cancelResponseBody(response);
|
|
109
|
+
throw responseAbortError(signal);
|
|
110
|
+
}
|
|
111
|
+
if (responseContentLength(response) > limit) {
|
|
112
|
+
await cancelResponseBody(response);
|
|
113
|
+
throw upstreamBodyTooLarge(label, limit);
|
|
114
|
+
}
|
|
115
|
+
if (!response?.body) {
|
|
116
|
+
if (typeof response?.text === "function") {
|
|
117
|
+
const buffer = Buffer.from(await response.text());
|
|
118
|
+
if (buffer.length > limit) throw upstreamBodyTooLarge(label, limit);
|
|
119
|
+
return buffer;
|
|
120
|
+
}
|
|
121
|
+
if ((responseContentLength(response) || 0) > 0) throw unreadableUpstreamBody(label);
|
|
122
|
+
return Buffer.alloc(0);
|
|
123
|
+
}
|
|
124
|
+
const reader = response?.body?.getReader?.();
|
|
125
|
+
if (!reader) throw unreadableUpstreamBody(label);
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let total = 0;
|
|
128
|
+
try {
|
|
129
|
+
while (true) {
|
|
130
|
+
const { done, value } = await readResponseChunk(reader, signal);
|
|
131
|
+
if (done) break;
|
|
132
|
+
const chunk = Buffer.from(value);
|
|
133
|
+
total += chunk.length;
|
|
134
|
+
if (total > limit) {
|
|
135
|
+
await reader.cancel().catch(() => {});
|
|
136
|
+
throw upstreamBodyTooLarge(label, limit);
|
|
137
|
+
}
|
|
138
|
+
chunks.push(chunk);
|
|
139
|
+
}
|
|
140
|
+
return Buffer.concat(chunks, total);
|
|
141
|
+
} finally {
|
|
142
|
+
try {
|
|
143
|
+
reader.releaseLock();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!signal?.aborted) throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function readBoundedResponseText(response, options) {
|
|
151
|
+
return new TextDecoder().decode(await readBoundedResponseBuffer(response, options));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function discardBoundedResponseBody(response, maxBytes = MAX_UPSTREAM_RETRY_DRAIN_BYTES) {
|
|
155
|
+
try {
|
|
156
|
+
await readBoundedResponseBuffer(response, { maxBytes, label: "Upstream retry response body" });
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
await cancelResponseBody(response);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
26
164
|
function payloadTooLarge(kind, maxBytes) {
|
|
27
165
|
return httpError(`${kind} request body exceeds ${maxBytes} bytes`, 413);
|
|
28
166
|
}
|
package/src/lock.mjs
CHANGED
|
@@ -32,23 +32,191 @@ function processIsAlive(pid) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function readStaleLockSnapshot(lockPath, io) {
|
|
36
|
+
const before = io.lstatSync(lockPath);
|
|
37
|
+
if (!before.isFile() || before.isSymbolicLink()) return { safe: false };
|
|
38
|
+
|
|
39
|
+
const noFollow = io.constants?.O_NOFOLLOW || 0;
|
|
40
|
+
let fd;
|
|
41
|
+
try {
|
|
42
|
+
fd = io.openSync(lockPath, io.constants.O_RDONLY | noFollow);
|
|
43
|
+
const opened = io.fstatSync(fd);
|
|
44
|
+
if (!opened.isFile()
|
|
45
|
+
|| opened.dev !== before.dev
|
|
46
|
+
|| opened.ino !== before.ino
|
|
47
|
+
|| opened.size !== before.size) {
|
|
48
|
+
return { safe: false };
|
|
49
|
+
}
|
|
50
|
+
const text = io.readFileSync(fd, "utf8");
|
|
51
|
+
const after = io.fstatSync(fd);
|
|
52
|
+
if (after.dev !== opened.dev
|
|
53
|
+
|| after.ino !== opened.ino
|
|
54
|
+
|| after.size !== opened.size
|
|
55
|
+
|| after.mtimeMs !== opened.mtimeMs
|
|
56
|
+
|| after.ctimeMs !== opened.ctimeMs) {
|
|
57
|
+
return { safe: false };
|
|
58
|
+
}
|
|
59
|
+
let record = {};
|
|
60
|
+
try {
|
|
61
|
+
record = JSON.parse(text);
|
|
62
|
+
} catch {}
|
|
63
|
+
return {
|
|
64
|
+
safe: true,
|
|
65
|
+
dev: after.dev,
|
|
66
|
+
ino: after.ino,
|
|
67
|
+
mtimeMs: after.mtimeMs,
|
|
68
|
+
size: after.size,
|
|
69
|
+
text,
|
|
70
|
+
pid: Number(record?.pid),
|
|
71
|
+
owner: typeof record?.owner === "string" ? record.owner : "",
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
if (fd !== undefined) io.closeSync(fd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
function sameLock(left, right) {
|
|
36
79
|
return left.dev === right.dev
|
|
37
80
|
&& left.ino === right.ino
|
|
38
81
|
&& left.mtimeMs === right.mtimeMs
|
|
39
82
|
&& left.size === right.size
|
|
40
|
-
&& left.owner === right.owner
|
|
83
|
+
&& left.owner === right.owner
|
|
84
|
+
&& left.text === right.text;
|
|
41
85
|
}
|
|
42
86
|
|
|
43
|
-
function
|
|
44
|
-
|
|
87
|
+
function lockPathExists(lockPath, io) {
|
|
88
|
+
try {
|
|
89
|
+
io.lstatSync(lockPath);
|
|
90
|
+
return true;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error?.code === "ENOENT") return false;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeLockRecord(lockPath, record, io) {
|
|
98
|
+
const fd = io.openSync(lockPath, "wx", 0o600);
|
|
99
|
+
try {
|
|
100
|
+
io.writeFileSync(fd, JSON.stringify(record));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
io.closeSync(fd);
|
|
103
|
+
try { io.unlinkSync(lockPath); } catch {}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
return fd;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeOwnedLock(lockPath, owner, io) {
|
|
110
|
+
try {
|
|
111
|
+
const snapshot = readStaleLockSnapshot(lockPath, io);
|
|
112
|
+
if (snapshot.safe && snapshot.owner === owner) io.unlinkSync(lockPath);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code !== "ENOENT") throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reclaimGuardPath(lockPath) {
|
|
119
|
+
return `${lockPath}.reclaim`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function acquireReclaimGuard(lockPath, io) {
|
|
123
|
+
const guardPath = reclaimGuardPath(lockPath);
|
|
124
|
+
const owner = randomUUID();
|
|
125
|
+
let fd;
|
|
126
|
+
try {
|
|
127
|
+
fd = writeLockRecord(guardPath, {
|
|
128
|
+
pid: process.pid,
|
|
129
|
+
owner,
|
|
130
|
+
created_at: new Date().toISOString(),
|
|
131
|
+
}, io);
|
|
132
|
+
io.closeSync(fd);
|
|
133
|
+
return { guardPath, owner };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (fd !== undefined) try { io.closeSync(fd); } catch {}
|
|
136
|
+
if (error?.code === "EEXIST") return null;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function maybeRemoveAbandonedGuard(guardPath, staleMs, nowMs, io, processAlive) {
|
|
142
|
+
try {
|
|
143
|
+
const snapshot = readStaleLockSnapshot(guardPath, io);
|
|
144
|
+
if (!snapshot.safe || nowMs - snapshot.mtimeMs <= staleMs || processAlive(snapshot.pid)) return false;
|
|
145
|
+
const quarantinePath = `${guardPath}.stale-${randomUUID()}`;
|
|
146
|
+
io.renameSync(guardPath, quarantinePath);
|
|
147
|
+
const moved = readStaleLockSnapshot(quarantinePath, io);
|
|
148
|
+
if (!moved.safe || !sameLock(snapshot, moved)) {
|
|
149
|
+
restoreQuarantinedLock(guardPath, quarantinePath, io);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
io.unlinkSync(quarantinePath);
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return recoverQuarantineUnlinkFailure(guardPath, quarantinePath, snapshot, io);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return error?.code === "ENOENT";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function restoreQuarantinedLock(lockPath, quarantinePath, io) {
|
|
164
|
+
try {
|
|
165
|
+
io.linkSync(quarantinePath, lockPath);
|
|
166
|
+
} catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
45
169
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
170
|
+
io.unlinkSync(quarantinePath);
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function recoverQuarantineUnlinkFailure(lockPath, quarantinePath, snapshot, io) {
|
|
175
|
+
restoreQuarantinedLock(lockPath, quarantinePath, io);
|
|
176
|
+
try {
|
|
177
|
+
const restored = readStaleLockSnapshot(lockPath, io);
|
|
178
|
+
if (!restored.safe || !sameLock(snapshot, restored)) return false;
|
|
179
|
+
io.unlinkSync(lockPath);
|
|
51
180
|
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return error?.code === "ENOENT";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function maybeRemoveStaleLock(lockPath, staleMs, nowMs, io, processAlive) {
|
|
187
|
+
if (!Number.isFinite(staleMs) || staleMs <= 0) return false;
|
|
188
|
+
try {
|
|
189
|
+
const first = readStaleLockSnapshot(lockPath, io);
|
|
190
|
+
if (!first.safe || nowMs - first.mtimeMs <= staleMs || processAlive(first.pid)) return false;
|
|
191
|
+
const guard = acquireReclaimGuard(lockPath, io);
|
|
192
|
+
if (!guard) return false;
|
|
193
|
+
try {
|
|
194
|
+
const current = readStaleLockSnapshot(lockPath, io);
|
|
195
|
+
if (!sameLock(first, current)
|
|
196
|
+
|| nowMs - current.mtimeMs <= staleMs
|
|
197
|
+
|| processAlive(current.pid)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const quarantinePath = `${lockPath}.stale-${randomUUID()}`;
|
|
201
|
+
try {
|
|
202
|
+
io.renameSync(lockPath, quarantinePath);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return e?.code === "ENOENT";
|
|
205
|
+
}
|
|
206
|
+
const moved = readStaleLockSnapshot(quarantinePath, io);
|
|
207
|
+
if (!moved.safe || !sameLock(current, moved)) {
|
|
208
|
+
restoreQuarantinedLock(lockPath, quarantinePath, io);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
io.unlinkSync(quarantinePath);
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return recoverQuarantineUnlinkFailure(lockPath, quarantinePath, current, io);
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
removeOwnedLock(guard.guardPath, guard.owner, io);
|
|
219
|
+
}
|
|
52
220
|
} catch (e) {
|
|
53
221
|
if (e?.code === "ENOENT") return true;
|
|
54
222
|
return false;
|
|
@@ -60,28 +228,42 @@ export async function withFileLock(lockPath, fn, {
|
|
|
60
228
|
staleMs = 15 * 60 * 1000,
|
|
61
229
|
pollMs = 50,
|
|
62
230
|
now = Date.now,
|
|
231
|
+
fsImpl = fs,
|
|
232
|
+
processAlive = processIsAlive,
|
|
63
233
|
} = {}) {
|
|
64
234
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
65
235
|
const started = now();
|
|
66
236
|
const owner = randomUUID();
|
|
67
237
|
let fd = null;
|
|
238
|
+
const guardPath = reclaimGuardPath(lockPath);
|
|
68
239
|
|
|
69
240
|
while (fd === null) {
|
|
241
|
+
const beforeAcquire = now();
|
|
242
|
+
if (lockPathExists(guardPath, fsImpl)) {
|
|
243
|
+
maybeRemoveAbandonedGuard(guardPath, staleMs, beforeAcquire, fsImpl, processAlive);
|
|
244
|
+
if (lockPathExists(guardPath, fsImpl)) {
|
|
245
|
+
if (beforeAcquire - started >= timeoutMs) throw new Error(`Timed out waiting for lock: ${lockPath}`);
|
|
246
|
+
await sleep(Math.min(pollMs, Math.max(1, timeoutMs - (beforeAcquire - started))));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
70
250
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
251
|
+
fd = writeLockRecord(lockPath, {
|
|
252
|
+
pid: process.pid,
|
|
253
|
+
owner,
|
|
254
|
+
created_at: new Date().toISOString(),
|
|
255
|
+
}, fs);
|
|
256
|
+
if (lockPathExists(guardPath, fsImpl)) {
|
|
257
|
+
fs.closeSync(fd);
|
|
258
|
+
fd = null;
|
|
259
|
+
removeOwnedLock(lockPath, owner, fs);
|
|
260
|
+
continue;
|
|
79
261
|
}
|
|
80
262
|
break;
|
|
81
263
|
} catch (e) {
|
|
82
264
|
if (e?.code !== "EEXIST") throw e;
|
|
83
265
|
const nowMs = now();
|
|
84
|
-
if (maybeRemoveStaleLock(lockPath, staleMs, nowMs)) continue;
|
|
266
|
+
if (maybeRemoveStaleLock(lockPath, staleMs, nowMs, fsImpl, processAlive)) continue;
|
|
85
267
|
if (nowMs - started >= timeoutMs) {
|
|
86
268
|
throw new Error(`Timed out waiting for lock: ${lockPath}`);
|
|
87
269
|
}
|
package/src/models.mjs
CHANGED
|
@@ -117,6 +117,23 @@ export function modelIsResponsesOnly(model) {
|
|
|
117
117
|
return hasResponses && !endpoints.includes("/chat/completions");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export function resolveCopilotPriorityTierModel(model, serviceTier, models) {
|
|
121
|
+
const baseModel = String(model || "").trim();
|
|
122
|
+
if (baseModel !== "gpt-5.6-sol" || serviceTier !== "priority") return null;
|
|
123
|
+
const fastModelId = "gpt-5.6-sol-fast";
|
|
124
|
+
const fastModel = copilotModelData(models)
|
|
125
|
+
.find((entry) => String(entry?.id || "").trim() === fastModelId);
|
|
126
|
+
const vendor = String(fastModel?.vendor || fastModel?.owned_by || "").trim().toLowerCase();
|
|
127
|
+
const policy = String(fastModel?.policy?.state || "").trim().toLowerCase();
|
|
128
|
+
if (vendor !== "openai"
|
|
129
|
+
|| policy !== "enabled"
|
|
130
|
+
|| fastModel?.model_picker_enabled !== true
|
|
131
|
+
|| !modelIsResponsesOnly(fastModel)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return fastModelId;
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
function copilotModelData(models) {
|
|
121
138
|
const data = Array.isArray(models) ? models : models?.data;
|
|
122
139
|
return Array.isArray(data) ? data : [];
|