codex-copilot-dx 0.6.15 → 0.6.18
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 +26 -15
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +28 -7
- package/src/anthropic-handler.mjs +17 -3
- package/src/anthropic.mjs +9 -4
- 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 +28 -71
- 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 +91 -0
- package/src/lock.mjs +179 -17
- package/src/models.mjs +17 -0
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-setup.mjs +319 -14
- 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 +117 -35
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +8 -1
- package/src/responses-request.mjs +17 -5
- package/src/usage.mjs +25 -7
package/src/http-transport.mjs
CHANGED
|
@@ -17,12 +17,103 @@ 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
|
+
|
|
20
24
|
export function httpError(message, statusCode) {
|
|
21
25
|
const err = new Error(message);
|
|
22
26
|
err.statusCode = statusCode;
|
|
23
27
|
return err;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
function upstreamBodyTooLarge(label, maxBytes) {
|
|
31
|
+
const error = httpError(`${label} exceeds ${maxBytes} bytes`, 502);
|
|
32
|
+
error.code = "ccdx_upstream_response_too_large";
|
|
33
|
+
error.jsonBody = {
|
|
34
|
+
error: {
|
|
35
|
+
message: error.message,
|
|
36
|
+
type: "upstream_error",
|
|
37
|
+
code: error.code,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unreadableUpstreamBody(label) {
|
|
44
|
+
const error = httpError(`${label} is not a readable web stream`, 502);
|
|
45
|
+
error.code = "ccdx_upstream_response_unreadable";
|
|
46
|
+
error.jsonBody = {
|
|
47
|
+
error: {
|
|
48
|
+
message: error.message,
|
|
49
|
+
type: "upstream_error",
|
|
50
|
+
code: error.code,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function cancelResponseBody(response) {
|
|
57
|
+
try { await response?.body?.cancel?.(); } catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function responseContentLength(response) {
|
|
61
|
+
const raw = response?.headers?.get?.("content-length")
|
|
62
|
+
?? response?.headers?.["content-length"]
|
|
63
|
+
?? response?.headers?.["Content-Length"];
|
|
64
|
+
const parsed = Number.parseInt(raw || "", 10);
|
|
65
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function readBoundedResponseBuffer(response, {
|
|
69
|
+
maxBytes,
|
|
70
|
+
label = "Upstream response body",
|
|
71
|
+
} = {}) {
|
|
72
|
+
const limit = Number.isFinite(maxBytes) && maxBytes >= 0 ? Math.floor(maxBytes) : 0;
|
|
73
|
+
if (responseContentLength(response) > limit) {
|
|
74
|
+
await cancelResponseBody(response);
|
|
75
|
+
throw upstreamBodyTooLarge(label, limit);
|
|
76
|
+
}
|
|
77
|
+
if (!response?.body) {
|
|
78
|
+
if ((responseContentLength(response) || 0) > 0) throw unreadableUpstreamBody(label);
|
|
79
|
+
return Buffer.alloc(0);
|
|
80
|
+
}
|
|
81
|
+
const reader = response?.body?.getReader?.();
|
|
82
|
+
if (!reader) throw unreadableUpstreamBody(label);
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let total = 0;
|
|
85
|
+
try {
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
if (done) break;
|
|
89
|
+
const chunk = Buffer.from(value);
|
|
90
|
+
total += chunk.length;
|
|
91
|
+
if (total > limit) {
|
|
92
|
+
await reader.cancel().catch(() => {});
|
|
93
|
+
throw upstreamBodyTooLarge(label, limit);
|
|
94
|
+
}
|
|
95
|
+
chunks.push(chunk);
|
|
96
|
+
}
|
|
97
|
+
return Buffer.concat(chunks, total);
|
|
98
|
+
} finally {
|
|
99
|
+
reader.releaseLock();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function readBoundedResponseText(response, options) {
|
|
104
|
+
return new TextDecoder().decode(await readBoundedResponseBuffer(response, options));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function discardBoundedResponseBody(response, maxBytes = MAX_UPSTREAM_RETRY_DRAIN_BYTES) {
|
|
108
|
+
try {
|
|
109
|
+
await readBoundedResponseBuffer(response, { maxBytes, label: "Upstream retry response body" });
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
await cancelResponseBody(response);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
26
117
|
function payloadTooLarge(kind, maxBytes) {
|
|
27
118
|
return httpError(`${kind} request body exceeds ${maxBytes} bytes`, 413);
|
|
28
119
|
}
|
package/src/lock.mjs
CHANGED
|
@@ -32,23 +32,171 @@ 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) {
|
|
45
142
|
try {
|
|
46
|
-
const
|
|
47
|
-
if (nowMs -
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
io.unlinkSync(quarantinePath);
|
|
51
153
|
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return error?.code === "ENOENT";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function restoreQuarantinedLock(lockPath, quarantinePath, io) {
|
|
160
|
+
try {
|
|
161
|
+
io.linkSync(quarantinePath, lockPath);
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
io.unlinkSync(quarantinePath);
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function maybeRemoveStaleLock(lockPath, staleMs, nowMs, io, processAlive) {
|
|
171
|
+
if (!Number.isFinite(staleMs) || staleMs <= 0) return false;
|
|
172
|
+
try {
|
|
173
|
+
const first = readStaleLockSnapshot(lockPath, io);
|
|
174
|
+
if (!first.safe || nowMs - first.mtimeMs <= staleMs || processAlive(first.pid)) return false;
|
|
175
|
+
const guard = acquireReclaimGuard(lockPath, io);
|
|
176
|
+
if (!guard) return false;
|
|
177
|
+
try {
|
|
178
|
+
const current = readStaleLockSnapshot(lockPath, io);
|
|
179
|
+
if (!sameLock(first, current)
|
|
180
|
+
|| nowMs - current.mtimeMs <= staleMs
|
|
181
|
+
|| processAlive(current.pid)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const quarantinePath = `${lockPath}.stale-${randomUUID()}`;
|
|
185
|
+
try {
|
|
186
|
+
io.renameSync(lockPath, quarantinePath);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return e?.code === "ENOENT";
|
|
189
|
+
}
|
|
190
|
+
const moved = readStaleLockSnapshot(quarantinePath, io);
|
|
191
|
+
if (!moved.safe || !sameLock(current, moved)) {
|
|
192
|
+
restoreQuarantinedLock(lockPath, quarantinePath, io);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
io.unlinkSync(quarantinePath);
|
|
196
|
+
return true;
|
|
197
|
+
} finally {
|
|
198
|
+
removeOwnedLock(guard.guardPath, guard.owner, io);
|
|
199
|
+
}
|
|
52
200
|
} catch (e) {
|
|
53
201
|
if (e?.code === "ENOENT") return true;
|
|
54
202
|
return false;
|
|
@@ -60,28 +208,42 @@ export async function withFileLock(lockPath, fn, {
|
|
|
60
208
|
staleMs = 15 * 60 * 1000,
|
|
61
209
|
pollMs = 50,
|
|
62
210
|
now = Date.now,
|
|
211
|
+
fsImpl = fs,
|
|
212
|
+
processAlive = processIsAlive,
|
|
63
213
|
} = {}) {
|
|
64
214
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
65
215
|
const started = now();
|
|
66
216
|
const owner = randomUUID();
|
|
67
217
|
let fd = null;
|
|
218
|
+
const guardPath = reclaimGuardPath(lockPath);
|
|
68
219
|
|
|
69
220
|
while (fd === null) {
|
|
221
|
+
const beforeAcquire = now();
|
|
222
|
+
if (lockPathExists(guardPath, fsImpl)) {
|
|
223
|
+
maybeRemoveAbandonedGuard(guardPath, staleMs, beforeAcquire, fsImpl, processAlive);
|
|
224
|
+
if (lockPathExists(guardPath, fsImpl)) {
|
|
225
|
+
if (beforeAcquire - started >= timeoutMs) throw new Error(`Timed out waiting for lock: ${lockPath}`);
|
|
226
|
+
await sleep(Math.min(pollMs, Math.max(1, timeoutMs - (beforeAcquire - started))));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
70
230
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
231
|
+
fd = writeLockRecord(lockPath, {
|
|
232
|
+
pid: process.pid,
|
|
233
|
+
owner,
|
|
234
|
+
created_at: new Date().toISOString(),
|
|
235
|
+
}, fs);
|
|
236
|
+
if (lockPathExists(guardPath, fsImpl)) {
|
|
237
|
+
fs.closeSync(fd);
|
|
238
|
+
fd = null;
|
|
239
|
+
removeOwnedLock(lockPath, owner, fs);
|
|
240
|
+
continue;
|
|
79
241
|
}
|
|
80
242
|
break;
|
|
81
243
|
} catch (e) {
|
|
82
244
|
if (e?.code !== "EEXIST") throw e;
|
|
83
245
|
const nowMs = now();
|
|
84
|
-
if (maybeRemoveStaleLock(lockPath, staleMs, nowMs)) continue;
|
|
246
|
+
if (maybeRemoveStaleLock(lockPath, staleMs, nowMs, fsImpl, processAlive)) continue;
|
|
85
247
|
if (nowMs - started >= timeoutMs) {
|
|
86
248
|
throw new Error(`Timed out waiting for lock: ${lockPath}`);
|
|
87
249
|
}
|
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 : [];
|