codex-copilot-dx 0.6.14 → 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 +30 -16
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +34 -9
- package/src/anthropic-handler.mjs +54 -4
- package/src/anthropic.mjs +64 -26
- 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 +80 -0
- package/src/copilot-responses-policy.mjs +78 -0
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +91 -0
- package/src/lock.mjs +179 -17
- package/src/model-cache.mjs +36 -6
- package/src/models.mjs +17 -0
- package/src/observability.mjs +13 -1
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +6 -3
- package/src/pm-studio-setup.mjs +319 -14
- package/src/pm-studio-status.mjs +5 -1
- package/src/profile-model-runtime.mjs +87 -13
- package/src/profile-routing.mjs +45 -7
- package/src/response-history.mjs +142 -16
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +154 -39
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +10 -2
- package/src/responses-request.mjs +109 -103
- package/src/route-plan.mjs +51 -0
- package/src/startup.mjs +8 -2
- package/src/usage.mjs +25 -7
package/src/profile-routing.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
isClaudeCopilotCatalogEntry,
|
|
3
3
|
isClaudeCopilotModel,
|
|
4
4
|
} from "./models.mjs";
|
|
5
|
+
import { createRoutePlan } from "./route-plan.mjs";
|
|
5
6
|
|
|
6
7
|
function catalogData(catalog) {
|
|
7
8
|
if (Array.isArray(catalog)) return catalog;
|
|
@@ -47,14 +48,51 @@ export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {})
|
|
|
47
48
|
return { allowed, known };
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function classify(modelId) {
|
|
52
|
+
const id = String(modelId || "").trim();
|
|
53
|
+
const { allowed, known } = availability();
|
|
54
|
+
if (allowed.has(id)) return "claude";
|
|
55
|
+
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
56
|
+
return "enterprise";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function plan(modelId) {
|
|
60
|
+
const id = String(modelId || "").trim();
|
|
61
|
+
const modelType = classify(id);
|
|
62
|
+
const protocol = "openai-chat-completions";
|
|
63
|
+
if (modelType === "claude") {
|
|
64
|
+
return createRoutePlan({
|
|
65
|
+
disposition: "relay",
|
|
66
|
+
origin: "ccdx",
|
|
67
|
+
profile: "claude",
|
|
68
|
+
protocol,
|
|
69
|
+
model: id,
|
|
70
|
+
surface: "pm-studio",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (modelType === "unsupported_claude") {
|
|
74
|
+
return createRoutePlan({
|
|
75
|
+
disposition: "reject",
|
|
76
|
+
origin: "ccdx",
|
|
77
|
+
profile: "claude",
|
|
78
|
+
protocol,
|
|
79
|
+
model: id || "unknown",
|
|
80
|
+
surface: "pm-studio",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return createRoutePlan({
|
|
84
|
+
disposition: "native",
|
|
85
|
+
origin: "pm-studio-native",
|
|
86
|
+
profile: "enterprise",
|
|
87
|
+
protocol,
|
|
88
|
+
model: id || "unknown",
|
|
89
|
+
surface: "pm-studio",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
return Object.freeze({
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const { allowed, known } = availability();
|
|
54
|
-
if (allowed.has(id)) return "claude";
|
|
55
|
-
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
56
|
-
return "enterprise";
|
|
57
|
-
},
|
|
94
|
+
plan,
|
|
95
|
+
classify,
|
|
58
96
|
allowedModels() {
|
|
59
97
|
return [...availability().allowed.values()];
|
|
60
98
|
},
|
package/src/response-history.mjs
CHANGED
|
@@ -71,6 +71,7 @@ function jsonByteLength(value, seen = new Set()) {
|
|
|
71
71
|
const histories = new Map();
|
|
72
72
|
const childrenById = new Map();
|
|
73
73
|
const treeLru = new Map();
|
|
74
|
+
const pinnedTrees = new Map();
|
|
74
75
|
const evictedIds = new Set();
|
|
75
76
|
let totalBytes = 0;
|
|
76
77
|
const HISTORY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
@@ -88,6 +89,21 @@ function touchTree(rootId) {
|
|
|
88
89
|
treeLru.set(rootId, true);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function pinTree(rootId) {
|
|
93
|
+
if (!rootId) return;
|
|
94
|
+
pinnedTrees.set(rootId, (pinnedTrees.get(rootId) || 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function unpinTree(rootId) {
|
|
98
|
+
const count = pinnedTrees.get(rootId) || 0;
|
|
99
|
+
if (count <= 1) pinnedTrees.delete(rootId);
|
|
100
|
+
else pinnedTrees.set(rootId, count - 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function treeIsPinned(rootId) {
|
|
104
|
+
return (pinnedTrees.get(rootId) || 0) > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
function linkChild(parentId, id) {
|
|
92
108
|
if (!parentId) return;
|
|
93
109
|
let children = childrenById.get(parentId);
|
|
@@ -148,18 +164,50 @@ function assignSubtreeRoot(id, rootId) {
|
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
function subtreeUsage(rootId) {
|
|
168
|
+
const pending = [rootId];
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
let bytes = 0;
|
|
171
|
+
let entries = 0;
|
|
172
|
+
while (pending.length) {
|
|
173
|
+
const id = pending.pop();
|
|
174
|
+
if (seen.has(id)) continue;
|
|
175
|
+
seen.add(id);
|
|
176
|
+
const entry = histories.get(id);
|
|
177
|
+
if (entry) {
|
|
178
|
+
entries += 1;
|
|
179
|
+
bytes += entry.bytes;
|
|
180
|
+
}
|
|
181
|
+
const children = childrenById.get(id);
|
|
182
|
+
if (children) pending.push(...children);
|
|
183
|
+
}
|
|
184
|
+
return { bytes, entries };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function makeRoomFor({ additionalBytes, additionalEntries, protectedRoots }) {
|
|
188
|
+
let projectedBytes = totalBytes + additionalBytes;
|
|
189
|
+
let projectedEntries = histories.size + additionalEntries;
|
|
190
|
+
if (projectedEntries <= maxEntries && projectedBytes <= maxBytes) return true;
|
|
191
|
+
|
|
192
|
+
const candidates = [];
|
|
193
|
+
for (const rootId of treeLru.keys()) {
|
|
194
|
+
if (protectedRoots.has(rootId) || treeIsPinned(rootId)) continue;
|
|
195
|
+
const usage = subtreeUsage(rootId);
|
|
196
|
+
candidates.push(rootId);
|
|
197
|
+
projectedBytes -= usage.bytes;
|
|
198
|
+
projectedEntries -= usage.entries;
|
|
199
|
+
if (projectedEntries <= maxEntries && projectedBytes <= maxBytes) break;
|
|
156
200
|
}
|
|
201
|
+
if (projectedEntries > maxEntries || projectedBytes > maxBytes) return false;
|
|
202
|
+
for (const rootId of candidates) removeSubtree(rootId);
|
|
203
|
+
return true;
|
|
157
204
|
}
|
|
158
205
|
|
|
159
206
|
export function clearResponseHistoryForTests() {
|
|
160
207
|
histories.clear();
|
|
161
208
|
childrenById.clear();
|
|
162
209
|
treeLru.clear();
|
|
210
|
+
pinnedTrees.clear();
|
|
163
211
|
evictedIds.clear();
|
|
164
212
|
totalBytes = 0;
|
|
165
213
|
maxBytes = DEFAULT_MAX_BYTES;
|
|
@@ -202,13 +250,61 @@ export function responseHistoryMaterializedBytes(responseId) {
|
|
|
202
250
|
return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
|
|
203
251
|
}
|
|
204
252
|
|
|
205
|
-
export function
|
|
253
|
+
export function acquireResponseHistorySnapshot(responseId, { assertActive, signal } = {}) {
|
|
254
|
+
assertActive?.();
|
|
255
|
+
if (signal?.aborted) throw signal.reason || new DOMException("The operation was aborted", "AbortError");
|
|
256
|
+
const { chain, rootId } = responseHistoryChain(responseId);
|
|
257
|
+
const entries = chain.slice().reverse();
|
|
258
|
+
const bytes = chain.reduce((total, entry) => total + entry.bytes, 0);
|
|
259
|
+
pinTree(rootId);
|
|
260
|
+
|
|
261
|
+
let released = false;
|
|
262
|
+
const release = () => {
|
|
263
|
+
if (released) return;
|
|
264
|
+
released = true;
|
|
265
|
+
signal?.removeEventListener("abort", release);
|
|
266
|
+
unpinTree(rootId);
|
|
267
|
+
};
|
|
268
|
+
signal?.addEventListener("abort", release, { once: true });
|
|
269
|
+
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
responseId,
|
|
272
|
+
rootId,
|
|
273
|
+
bytes,
|
|
274
|
+
materialize({ assertActive: assertSnapshotActive, routeMetadata } = {}) {
|
|
275
|
+
assertSnapshotActive?.();
|
|
276
|
+
if (released) throw new Error("Response history snapshot has been released");
|
|
277
|
+
const items = [];
|
|
278
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
279
|
+
if ((index & 63) === 0) assertSnapshotActive?.();
|
|
280
|
+
const entry = entries[index];
|
|
281
|
+
routeMetadata?.push({
|
|
282
|
+
affinity: entry.routeAffinity || null,
|
|
283
|
+
hasOpaque: entry.hasOpaque === true,
|
|
284
|
+
});
|
|
285
|
+
items.push(...entry.inputItems, ...entry.outputItems);
|
|
286
|
+
}
|
|
287
|
+
assertSnapshotActive?.();
|
|
288
|
+
const materialized = cloneJson(items);
|
|
289
|
+
assertSnapshotActive?.();
|
|
290
|
+
touchTree(rootId);
|
|
291
|
+
return materialized;
|
|
292
|
+
},
|
|
293
|
+
release,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function materializeResponseHistory(responseId, { assertActive, routeMetadata } = {}) {
|
|
206
298
|
assertActive?.();
|
|
207
299
|
const { chain, rootId } = responseHistoryChain(responseId);
|
|
208
300
|
const items = [];
|
|
209
301
|
for (let index = 0; index < chain.length; index += 1) {
|
|
210
302
|
if ((index & 63) === 0) assertActive?.();
|
|
211
303
|
const entry = chain[chain.length - index - 1];
|
|
304
|
+
routeMetadata?.push({
|
|
305
|
+
affinity: entry.routeAffinity || null,
|
|
306
|
+
hasOpaque: entry.hasOpaque === true,
|
|
307
|
+
});
|
|
212
308
|
items.push(...entry.inputItems, ...entry.outputItems);
|
|
213
309
|
}
|
|
214
310
|
assertActive?.();
|
|
@@ -218,27 +314,57 @@ export function materializeResponseHistory(responseId, { assertActive } = {}) {
|
|
|
218
314
|
return materialized;
|
|
219
315
|
}
|
|
220
316
|
|
|
221
|
-
export function rememberResponseHistoryNode({
|
|
222
|
-
|
|
317
|
+
export function rememberResponseHistoryNode({
|
|
318
|
+
id,
|
|
319
|
+
parentId,
|
|
320
|
+
inputItems,
|
|
321
|
+
outputItems,
|
|
322
|
+
hasOpaque = false,
|
|
323
|
+
routeAffinity = null,
|
|
324
|
+
takeOwnership = false,
|
|
325
|
+
}) {
|
|
326
|
+
if (!id || !Array.isArray(inputItems)) return false;
|
|
327
|
+
const existing = histories.get(id);
|
|
328
|
+
const parentEntry = parentId ? histories.get(parentId) : null;
|
|
329
|
+
if (parentId && !parentEntry) {
|
|
330
|
+
if (!existing) rememberEvictedId(id);
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
223
333
|
clearResponsesToolOutputPartsCache(inputItems);
|
|
224
334
|
clearResponsesToolOutputPartsCache(outputItems);
|
|
225
|
-
const
|
|
335
|
+
const historyValue = routeAffinity
|
|
336
|
+
? [inputItems, outputItems, { routeAffinity, hasOpaque: hasOpaque === true }]
|
|
337
|
+
: [inputItems, outputItems];
|
|
338
|
+
const bytes = jsonByteLength(historyValue);
|
|
226
339
|
if (bytes > maxBytes) {
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
return;
|
|
340
|
+
if (!existing) rememberEvictedId(id);
|
|
341
|
+
else if (!treeIsPinned(existing.rootId)) removeSubtree(id);
|
|
342
|
+
return false;
|
|
230
343
|
}
|
|
231
|
-
const
|
|
232
|
-
const parentEntry = parentId ? histories.get(parentId) : null;
|
|
233
|
-
const rootId = parentEntry?.rootId || (parentId ? existing?.rootId : id) || id;
|
|
344
|
+
const rootId = parentEntry?.rootId || id;
|
|
234
345
|
const entry = {
|
|
235
346
|
parentId: parentId || null,
|
|
236
347
|
rootId,
|
|
237
348
|
inputItems: takeOwnership ? inputItems : cloneJson(inputItems),
|
|
238
349
|
outputItems: takeOwnership ? outputItems : cloneJson(outputItems),
|
|
350
|
+
hasOpaque: hasOpaque === true,
|
|
351
|
+
routeAffinity: routeAffinity ? cloneJson(routeAffinity) : null,
|
|
239
352
|
bytes,
|
|
240
353
|
};
|
|
241
354
|
const oldRootId = existing?.rootId;
|
|
355
|
+
const protectedRoots = new Set([rootId]);
|
|
356
|
+
if (oldRootId) protectedRoots.add(oldRootId);
|
|
357
|
+
if (!makeRoomFor({
|
|
358
|
+
additionalBytes: bytes - (existing?.bytes || 0),
|
|
359
|
+
additionalEntries: existing ? 0 : 1,
|
|
360
|
+
protectedRoots,
|
|
361
|
+
})) {
|
|
362
|
+
if (!existing) {
|
|
363
|
+
if (parentEntry && !treeIsPinned(rootId)) removeSubtree(rootId);
|
|
364
|
+
rememberEvictedId(id);
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
242
368
|
if (existing) {
|
|
243
369
|
totalBytes -= existing.bytes;
|
|
244
370
|
unlinkChild(existing.parentId, id);
|
|
@@ -253,5 +379,5 @@ export function rememberResponseHistoryNode({ id, parentId, inputItems, outputIt
|
|
|
253
379
|
totalBytes += entry.bytes;
|
|
254
380
|
evictedIds.delete(id);
|
|
255
381
|
touchTree(rootId);
|
|
256
|
-
|
|
382
|
+
return true;
|
|
257
383
|
}
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chatCompletions } from "./copilot.mjs";
|
|
3
3
|
import { webStreamLines } from "./stream.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
abortErrorStatusCode,
|
|
6
|
+
isAbortLikeError,
|
|
7
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
8
|
+
readBoundedResponseText,
|
|
9
|
+
} from "./http-transport.mjs";
|
|
5
10
|
import {
|
|
6
11
|
incompleteUpstreamStream,
|
|
7
12
|
invalidUpstreamStream,
|
|
@@ -216,7 +221,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
216
221
|
return false;
|
|
217
222
|
}
|
|
218
223
|
if (!resp.ok) {
|
|
219
|
-
await onError(resp.status, await resp
|
|
224
|
+
await onError(resp.status, await readBoundedResponseText(resp, {
|
|
225
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
226
|
+
label: "Copilot Chat error body",
|
|
227
|
+
}), undefined, resp);
|
|
220
228
|
return false;
|
|
221
229
|
}
|
|
222
230
|
try {
|
|
@@ -235,6 +243,13 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
235
243
|
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
236
244
|
const toolCalls = new Map();
|
|
237
245
|
const toolArgumentGuard = new ToolArgumentDeltaGuard();
|
|
246
|
+
const downstreamClosed = Symbol("downstream-closed");
|
|
247
|
+
const configuredEmitEvent = emitEvent;
|
|
248
|
+
emitEvent = async (...args) => {
|
|
249
|
+
const written = await configuredEmitEvent(...args);
|
|
250
|
+
if (written === false) throw downstreamClosed;
|
|
251
|
+
return written;
|
|
252
|
+
};
|
|
238
253
|
let performanceOutputSeen = false;
|
|
239
254
|
const markPerformanceOutput = () => {
|
|
240
255
|
if (performanceOutputSeen) return;
|
|
@@ -242,8 +257,6 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
242
257
|
markFirstOutput();
|
|
243
258
|
};
|
|
244
259
|
|
|
245
|
-
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
246
|
-
|
|
247
260
|
const ensureMessageItem = async () => {
|
|
248
261
|
if (messageItem) return messageItem;
|
|
249
262
|
messageItem = { id: `msg_${uid()}`, outputIndex: nextOutputIndex++ };
|
|
@@ -331,6 +344,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
331
344
|
};
|
|
332
345
|
|
|
333
346
|
try {
|
|
347
|
+
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
334
348
|
for await (const line of webStreamLines(resp, {
|
|
335
349
|
onChunk: () => abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
336
350
|
})) {
|
|
@@ -386,6 +400,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
386
400
|
}
|
|
387
401
|
}
|
|
388
402
|
} catch (e) {
|
|
403
|
+
if (e === downstreamClosed) {
|
|
404
|
+
await resp.body?.cancel?.().catch?.(() => {});
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
389
407
|
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : (e?.statusCode || 502);
|
|
390
408
|
await onError(statusCode, e?.message || "upstream stream error", e);
|
|
391
409
|
return false;
|