codex-copilot-dx 0.6.4 → 0.6.6
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 +20 -20
- package/bin/cli.mjs +1 -0
- package/package.json +1 -1
- package/scripts/benchmark.mjs +58 -1
- package/src/adapter.mjs +4 -5
- package/src/auto-review-model.mjs +1 -1
- package/src/cli-auth.mjs +2 -11
- package/src/cli-models.mjs +14 -7
- package/src/cli-options.mjs +1 -1
- package/src/copilot-client.mjs +3 -1
- package/src/doctor.mjs +9 -8
- package/src/http-transport.mjs +114 -13
- package/src/image-optimization.mjs +33 -11
- package/src/launcher.mjs +2 -3
- package/src/legacy-command-warning.mjs +3 -2
- package/src/models.mjs +14 -10
- package/src/observability.mjs +18 -1
- package/src/pm-studio-setup.mjs +28 -4
- package/src/pm-studio-status.mjs +103 -9
- package/src/profile-routing.mjs +7 -16
- package/src/profile-runtime.mjs +12 -3
- package/src/response-history.mjs +12 -2
- package/src/responses-byte-budget.mjs +53 -26
- package/src/responses-chat-payload.mjs +35 -16
- package/src/responses-content.mjs +36 -4
- package/src/responses-handler.mjs +47 -27
- package/src/responses-image-limit.mjs +16 -2
- package/src/responses-image-pressure.mjs +10 -4
- package/src/responses-request.mjs +28 -10
|
@@ -273,9 +273,11 @@ export async function runWithConcurrency(taskFns, concurrency) {
|
|
|
273
273
|
}));
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
function visitImageParts(parts, references, commit = null) {
|
|
276
|
+
function visitImageParts(parts, references, commit = null, assertActive) {
|
|
277
277
|
if (!Array.isArray(parts)) return;
|
|
278
|
-
for (
|
|
278
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
279
|
+
if ((index & 63) === 0) assertActive?.();
|
|
280
|
+
const part = parts[index];
|
|
279
281
|
const image = readResponsesImagePart(part);
|
|
280
282
|
if (!image?.identity) continue;
|
|
281
283
|
references.push({
|
|
@@ -286,22 +288,24 @@ function visitImageParts(parts, references, commit = null) {
|
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
function collectImageReferences(reqBody) {
|
|
291
|
+
function collectImageReferences(reqBody, assertActive) {
|
|
290
292
|
const references = [];
|
|
291
293
|
const dirtyCommits = new Set();
|
|
292
294
|
if (!Array.isArray(reqBody?.input)) {
|
|
293
295
|
return { references, markDirty() {}, commitDirty() {} };
|
|
294
296
|
}
|
|
295
297
|
|
|
296
|
-
for (
|
|
298
|
+
for (let index = 0; index < reqBody.input.length; index += 1) {
|
|
299
|
+
if ((index & 63) === 0) assertActive?.();
|
|
300
|
+
const item = reqBody.input[index];
|
|
297
301
|
if (!item) continue;
|
|
298
|
-
visitImageParts([item], references);
|
|
302
|
+
visitImageParts([item], references, null, assertActive);
|
|
299
303
|
if (item.type === "message" && Array.isArray(item.content)) {
|
|
300
|
-
visitImageParts(item.content, references);
|
|
304
|
+
visitImageParts(item.content, references, null, assertActive);
|
|
301
305
|
}
|
|
302
306
|
const toolOutput = readResponsesToolOutputParts(item);
|
|
303
307
|
if (toolOutput) {
|
|
304
|
-
visitImageParts(toolOutput.parts, references, toolOutput.commit);
|
|
308
|
+
visitImageParts(toolOutput.parts, references, toolOutput.commit, assertActive);
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
|
|
@@ -320,6 +324,7 @@ function collectImageReferences(reqBody) {
|
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
async function optimizeImageReferences(collection, {
|
|
327
|
+
assertActive,
|
|
323
328
|
concurrency,
|
|
324
329
|
model,
|
|
325
330
|
optimizeImage,
|
|
@@ -334,7 +339,9 @@ async function optimizeImageReferences(collection, {
|
|
|
334
339
|
return optimizedByDataUrl.get(identity);
|
|
335
340
|
};
|
|
336
341
|
const tasks = collection.references.map((reference) => async () => {
|
|
342
|
+
assertActive?.();
|
|
337
343
|
if (reference.set(await optimizeOnce(reference.get()))) collection.markDirty([reference]);
|
|
344
|
+
assertActive?.();
|
|
338
345
|
});
|
|
339
346
|
if (tasks.length) await runWithConcurrency(tasks, concurrency);
|
|
340
347
|
collection.commitDirty();
|
|
@@ -346,9 +353,11 @@ function imageInputBytes(dataUrl) {
|
|
|
346
353
|
return match ? Buffer.byteLength(match[1], "base64") : 0;
|
|
347
354
|
}
|
|
348
355
|
|
|
349
|
-
function uniqueOriginalImages(references) {
|
|
356
|
+
function uniqueOriginalImages(references, assertActive) {
|
|
350
357
|
const unique = new Map();
|
|
351
|
-
for (
|
|
358
|
+
for (let index = 0; index < references.length; index += 1) {
|
|
359
|
+
if ((index & 63) === 0) assertActive?.();
|
|
360
|
+
const reference = references[index];
|
|
352
361
|
const dataUrl = reference.get();
|
|
353
362
|
const identity = canonicalInlineImageIdentity(dataUrl);
|
|
354
363
|
if (!identity) continue;
|
|
@@ -422,6 +431,7 @@ export async function optimizeImagesInBody(reqBody, {
|
|
|
422
431
|
}
|
|
423
432
|
|
|
424
433
|
export async function prepareResponsesPayload(reqBody, {
|
|
434
|
+
assertActive,
|
|
425
435
|
currentInputStart = 0,
|
|
426
436
|
maxBytes = MAX_UPSTREAM_BODY_BYTES,
|
|
427
437
|
optimizeImage = optimizeImageDataUrl,
|
|
@@ -429,18 +439,22 @@ export async function prepareResponsesPayload(reqBody, {
|
|
|
429
439
|
skipInitialOptimization = false,
|
|
430
440
|
signal,
|
|
431
441
|
} = {}) {
|
|
442
|
+
assertActive?.();
|
|
432
443
|
const shouldOptimizeImages = !IMG_OPT_DISABLED && !skipInitialOptimization;
|
|
433
|
-
const collection = shouldOptimizeImages ? collectImageReferences(reqBody) : null;
|
|
434
|
-
const originalImages = collection ? uniqueOriginalImages(collection.references) : [];
|
|
444
|
+
const collection = shouldOptimizeImages ? collectImageReferences(reqBody, assertActive) : null;
|
|
445
|
+
const originalImages = collection ? uniqueOriginalImages(collection.references, assertActive) : [];
|
|
435
446
|
if (collection) {
|
|
436
447
|
await optimizeImageReferences(collection, {
|
|
448
|
+
assertActive,
|
|
437
449
|
concurrency: IMG_CONCURRENCY,
|
|
438
450
|
model: reqBody.model,
|
|
439
451
|
optimizeImage: (dataUrl, options) => optimizeImage(dataUrl, { ...options, quality: IMG_QUALITY }),
|
|
440
452
|
signal,
|
|
441
453
|
});
|
|
442
454
|
}
|
|
455
|
+
assertActive?.();
|
|
443
456
|
let bodyText = JSON.stringify(reqBody);
|
|
457
|
+
assertActive?.();
|
|
444
458
|
let bodyBytes = Buffer.byteLength(bodyText);
|
|
445
459
|
const summary = summarizeReqBody(reqBody);
|
|
446
460
|
const targetBytes = positiveInt(maxBytes, MAX_UPSTREAM_BODY_BYTES);
|
|
@@ -450,11 +464,13 @@ export async function prepareResponsesPayload(reqBody, {
|
|
|
450
464
|
if (collection && originalImages.length > 0 && bodyBytes > targetBytes) {
|
|
451
465
|
const batchSize = Math.max(1, IMG_CONCURRENCY);
|
|
452
466
|
for (const profile of Array.isArray(profiles) ? profiles : []) {
|
|
467
|
+
assertActive?.();
|
|
453
468
|
const beforeBytes = bodyBytes;
|
|
454
469
|
let processed = 0;
|
|
455
470
|
stage = `q${positiveInt(profile.quality, IMG_QUALITY, 100)}`;
|
|
456
471
|
const orderedImages = orderImagesByCurrentWireBytes(originalImages);
|
|
457
472
|
for (let index = 0; index < orderedImages.length && bodyBytes > targetBytes; index += batchSize) {
|
|
473
|
+
assertActive?.();
|
|
458
474
|
const batch = orderedImages.slice(index, index + batchSize);
|
|
459
475
|
const candidates = await Promise.all(batch.map((image) => optimizeImage(image.dataUrl, {
|
|
460
476
|
...profile,
|
|
@@ -462,6 +478,7 @@ export async function prepareResponsesPayload(reqBody, {
|
|
|
462
478
|
model: reqBody.model,
|
|
463
479
|
signal,
|
|
464
480
|
})));
|
|
481
|
+
assertActive?.();
|
|
465
482
|
for (let offset = 0; offset < batch.length && bodyBytes > targetBytes; offset += 1) {
|
|
466
483
|
processed += 1;
|
|
467
484
|
const applied = applyImageCandidate(collection, batch[offset], candidates[offset], bodyBytes);
|
|
@@ -469,19 +486,24 @@ export async function prepareResponsesPayload(reqBody, {
|
|
|
469
486
|
adapted ||= applied.changed;
|
|
470
487
|
if (bodyBytes <= targetBytes) {
|
|
471
488
|
collection.commitDirty();
|
|
489
|
+
assertActive?.();
|
|
472
490
|
bodyText = JSON.stringify(reqBody);
|
|
491
|
+
assertActive?.();
|
|
473
492
|
bodyBytes = Buffer.byteLength(bodyText);
|
|
474
493
|
}
|
|
475
494
|
}
|
|
476
495
|
}
|
|
477
496
|
collection.commitDirty();
|
|
497
|
+
assertActive?.();
|
|
478
498
|
bodyText = JSON.stringify(reqBody);
|
|
499
|
+
assertActive?.();
|
|
479
500
|
bodyBytes = Buffer.byteLength(bodyText);
|
|
480
501
|
console.warn(status("warn", `responses payload ${beforeBytes}b exceeds ${targetBytes}b; image profile max_dim=${profile.maxDim} quality=${profile.quality} processed=${processed}/${orderedImages.length} -> ${bodyBytes}b`));
|
|
481
502
|
if (bodyBytes <= targetBytes) break;
|
|
482
503
|
}
|
|
483
504
|
}
|
|
484
505
|
|
|
506
|
+
assertActive?.();
|
|
485
507
|
return {
|
|
486
508
|
bodyText,
|
|
487
509
|
bodyBytes,
|
package/src/launcher.mjs
CHANGED
|
@@ -3,9 +3,8 @@ import { status } from "./status.mjs";
|
|
|
3
3
|
|
|
4
4
|
export { ensureAuth } from "./auth.mjs";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Codex.app and the new ChatGPT.app regardless of where they are installed.
|
|
6
|
+
// Prefer the Codex bundle identifier, then fall back to the known app paths.
|
|
7
|
+
// This supports both Codex.app and ChatGPT.app regardless of installation path.
|
|
9
8
|
const CODEX_BUNDLE_ID = "com.openai.codex";
|
|
10
9
|
const APP_PATH_CANDIDATES = [
|
|
11
10
|
"/Applications/Codex.app",
|
|
@@ -30,8 +30,9 @@ export function shouldShowLegacyCommandWarning({
|
|
|
30
30
|
}
|
|
31
31
|
try {
|
|
32
32
|
atomicWriteFileSync(filePath, `${now}\n`, { mode: 0o600 });
|
|
33
|
+
return true;
|
|
33
34
|
} catch {
|
|
34
|
-
//
|
|
35
|
+
// Stay silent when the throttle cannot persist, otherwise every invocation would warn.
|
|
36
|
+
return false;
|
|
35
37
|
}
|
|
36
|
-
return true;
|
|
37
38
|
}
|
package/src/models.mjs
CHANGED
|
@@ -95,11 +95,6 @@ function numericLimit(value, fallback) {
|
|
|
95
95
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function hasMessagesEndpoint(model) {
|
|
99
|
-
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
100
|
-
return endpoints.includes("/v1/messages") || endpoints.includes("/chat/completions");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
98
|
function hasOpenAIEndpoint(model) {
|
|
104
99
|
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
105
100
|
return endpoints.includes("/responses") || endpoints.includes("/v1/responses") || endpoints.includes("/chat/completions");
|
|
@@ -140,13 +135,22 @@ function uniqueIds(models, predicate) {
|
|
|
140
135
|
return ids;
|
|
141
136
|
}
|
|
142
137
|
|
|
143
|
-
function
|
|
138
|
+
export function isClaudeCopilotCatalogEntry(model) {
|
|
144
139
|
const id = String(model?.id || "").trim();
|
|
145
|
-
const vendor = String(model?.vendor || "").toLowerCase();
|
|
146
|
-
return id
|
|
147
|
-
|
|
140
|
+
const vendor = String(model?.vendor || model?.owned_by || "").trim().toLowerCase();
|
|
141
|
+
return Boolean(id) && (id.toLowerCase().startsWith("claude-") || vendor === "anthropic");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function isClaudeCopilotModel(model) {
|
|
145
|
+
const policy = String(model?.policy?.state || "").trim().toLowerCase();
|
|
146
|
+
const declaredOwners = [model?.vendor, model?.owned_by]
|
|
147
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
return isClaudeCopilotCatalogEntry(model)
|
|
150
|
+
&& declaredOwners.every((owner) => owner === "anthropic")
|
|
148
151
|
&& model?.model_picker_enabled !== false
|
|
149
|
-
&&
|
|
152
|
+
&& (!policy || policy === "enabled")
|
|
153
|
+
&& modelSupportsChatCompletions(model);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
export function gptModelIdsFromCopilotModels(models) {
|
package/src/observability.mjs
CHANGED
|
@@ -150,6 +150,15 @@ function safeClientRuntimeStatus(client) {
|
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function safeProfileCurrent(checkProfileCurrent) {
|
|
154
|
+
if (typeof checkProfileCurrent !== "function") return false;
|
|
155
|
+
try {
|
|
156
|
+
return checkProfileCurrent() === true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
153
162
|
export function runtimeStatusPayload({
|
|
154
163
|
metrics,
|
|
155
164
|
streamPerformance,
|
|
@@ -161,6 +170,7 @@ export function runtimeStatusPayload({
|
|
|
161
170
|
codexModelRegistry,
|
|
162
171
|
claudeModelRegistry,
|
|
163
172
|
claudeMode = "inherited",
|
|
173
|
+
isClaudeProfileCurrent,
|
|
164
174
|
} = {}) {
|
|
165
175
|
const memory = process.memoryUsage();
|
|
166
176
|
const resolvedClaudeMode = claudeMode === "isolated" ? "isolated" : "inherited";
|
|
@@ -172,6 +182,8 @@ export function runtimeStatusPayload({
|
|
|
172
182
|
const claudeModels = resolvedClaudeMode === "isolated"
|
|
173
183
|
? modelRegistryStatus(claudeModelRegistry)
|
|
174
184
|
: codexModels;
|
|
185
|
+
const claudeProfileCurrent = resolvedClaudeMode === "isolated"
|
|
186
|
+
&& safeProfileCurrent(isClaudeProfileCurrent);
|
|
175
187
|
return {
|
|
176
188
|
...adapterHealthPayload(),
|
|
177
189
|
uptime_ms: Math.round(process.uptime() * 1000),
|
|
@@ -192,7 +204,12 @@ export function runtimeStatusPayload({
|
|
|
192
204
|
models: codexModels,
|
|
193
205
|
profiles: {
|
|
194
206
|
codex: { mode: "legacy", client: codexRuntime, models: codexModels },
|
|
195
|
-
claude: {
|
|
207
|
+
claude: {
|
|
208
|
+
mode: resolvedClaudeMode,
|
|
209
|
+
profile_current: claudeProfileCurrent,
|
|
210
|
+
client: claudeRuntime,
|
|
211
|
+
models: claudeModels,
|
|
212
|
+
},
|
|
196
213
|
},
|
|
197
214
|
routing: profileRouting({ claudeMode: resolvedClaudeMode }),
|
|
198
215
|
limits: {
|
package/src/pm-studio-setup.mjs
CHANGED
|
@@ -396,6 +396,19 @@ function backupName(recipe) {
|
|
|
396
396
|
return `${recipe.id}-${recipe.sourceAsarSha256.slice(0, 12)}`;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
export function pmStudioBackupRoot(home = os.homedir()) {
|
|
400
|
+
return path.join(home, ".local", "share", "codex-copilot-dx", "pm-studio-backups");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function pmStudioPatchManifestPath({
|
|
404
|
+
home = os.homedir(),
|
|
405
|
+
backupRoot = pmStudioBackupRoot(home),
|
|
406
|
+
recipe,
|
|
407
|
+
} = {}) {
|
|
408
|
+
if (!recipe) throw new TypeError("PM Studio patch recipe is required");
|
|
409
|
+
return path.join(backupRoot, backupName(recipe), "manifest.json");
|
|
410
|
+
}
|
|
411
|
+
|
|
399
412
|
function backupManifest(recipe, createdAt) {
|
|
400
413
|
return {
|
|
401
414
|
schema_version: 1,
|
|
@@ -512,13 +525,24 @@ function writePatchedBinaryRecord({ backup, inspection, operations }) {
|
|
|
512
525
|
backup.manifest = manifest;
|
|
513
526
|
}
|
|
514
527
|
|
|
515
|
-
function assertPatchedBinaryRecord({ inspection, manifestPath }) {
|
|
528
|
+
export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe }) {
|
|
516
529
|
let manifest;
|
|
517
530
|
try {
|
|
518
531
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
519
532
|
} catch {
|
|
520
533
|
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
|
|
521
534
|
}
|
|
535
|
+
if (recipe && (manifest?.schema_version !== 1
|
|
536
|
+
|| manifest?.kind !== "ccdx-pm-studio-backup"
|
|
537
|
+
|| manifest?.recipe_id !== recipe.id
|
|
538
|
+
|| manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
|
|
539
|
+
|| manifest?.app?.version !== recipe.version
|
|
540
|
+
|| manifest?.app?.build !== recipe.build
|
|
541
|
+
|| manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
|
|
542
|
+
|| manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
|
|
543
|
+
|| !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
|
|
544
|
+
throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
|
|
545
|
+
}
|
|
522
546
|
const binaries = manifest?.patched?.binaries;
|
|
523
547
|
if (!binaries
|
|
524
548
|
|| binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
|
|
@@ -593,7 +617,7 @@ export async function runPmStudioSetup({
|
|
|
593
617
|
commandName = "ccdx",
|
|
594
618
|
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
595
619
|
home = os.homedir(),
|
|
596
|
-
backupRoot =
|
|
620
|
+
backupRoot = pmStudioBackupRoot(home),
|
|
597
621
|
recipes = PM_STUDIO_RECIPES,
|
|
598
622
|
operations: operationOverrides = {},
|
|
599
623
|
logger = (line) => console.log(line),
|
|
@@ -629,7 +653,7 @@ export async function runPmStudioSetup({
|
|
|
629
653
|
throw setupError("PM_STUDIO_BACKUP_INVALID", "Patched PM Studio has no matching verified backup; no files were changed");
|
|
630
654
|
}
|
|
631
655
|
const backup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
|
|
632
|
-
assertPatchedBinaryRecord({ inspection: source, manifestPath: backup.manifestPath });
|
|
656
|
+
assertPatchedBinaryRecord({ inspection: source, manifestPath: backup.manifestPath, recipe });
|
|
633
657
|
emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
|
|
634
658
|
emit(`[OK] Verified backup: ${backup.backupAppPath}`);
|
|
635
659
|
emit(`[OK] Backup manifest: ${backup.manifestPath}`);
|
|
@@ -700,7 +724,7 @@ export async function runPmStudioSetup({
|
|
|
700
724
|
installed.issues);
|
|
701
725
|
}
|
|
702
726
|
try {
|
|
703
|
-
assertPatchedBinaryRecord({ inspection: installed, manifestPath: backup.manifestPath });
|
|
727
|
+
assertPatchedBinaryRecord({ inspection: installed, manifestPath: backup.manifestPath, recipe });
|
|
704
728
|
} catch (error) {
|
|
705
729
|
throw setupError("PM_STUDIO_INSTALL_VERIFY_FAILED",
|
|
706
730
|
`Installed PM Studio differs from the verified staging record; do not launch it. ${recoveryMessage(backup)}`,
|
package/src/pm-studio-status.mjs
CHANGED
|
@@ -7,16 +7,27 @@ import {
|
|
|
7
7
|
} from "./auth-profile.mjs";
|
|
8
8
|
import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
|
|
9
9
|
import {
|
|
10
|
+
assertPatchedBinaryRecord,
|
|
10
11
|
createPmStudioSetupOperations,
|
|
11
12
|
DEFAULT_PM_STUDIO_APP_PATH,
|
|
12
13
|
inspectPmStudioApp,
|
|
13
14
|
PM_STUDIO_CLAUDE_AUTH_COMMAND,
|
|
15
|
+
pmStudioBackupRoot,
|
|
16
|
+
pmStudioPatchManifestPath,
|
|
14
17
|
} from "./pm-studio-setup.mjs";
|
|
18
|
+
import { readAdapterStatus } from "./cli-status.mjs";
|
|
15
19
|
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
16
20
|
import { status } from "./status.mjs";
|
|
17
21
|
|
|
18
22
|
const PM_RELAY_HOST = "127.0.0.1";
|
|
19
23
|
const PM_RELAY_PORT = 2026;
|
|
24
|
+
const PM_RELAY_CAPABILITY = "pm_studio_relay_v1";
|
|
25
|
+
const PM_RELAY_ROUTE_NAMES = Object.freeze([
|
|
26
|
+
"pm_models",
|
|
27
|
+
"pm_chat_completions",
|
|
28
|
+
"pm_responses",
|
|
29
|
+
"pm_embeddings",
|
|
30
|
+
]);
|
|
20
31
|
|
|
21
32
|
function selectRecipe(metadata, recipes) {
|
|
22
33
|
return recipes.find((recipe) => recipe.version === String(metadata.version)
|
|
@@ -42,15 +53,52 @@ function inspectClaudeProfile(home, readCredentials) {
|
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
|
|
56
|
+
function inspectAdapterRuntime(adapter, runtimeStatus) {
|
|
57
|
+
const data = runtimeStatus?.data;
|
|
58
|
+
const health = adapter?.data;
|
|
59
|
+
const issues = [];
|
|
60
|
+
if (!data || typeof data !== "object") {
|
|
61
|
+
return { ok: false, baseUrl: runtimeStatus?.baseUrl || adapter?.baseUrl, data: null, issues: ["runtime status payload is missing"] };
|
|
62
|
+
}
|
|
63
|
+
if (data.version !== health?.version
|
|
64
|
+
|| data.protocol_version !== health?.protocol_version
|
|
65
|
+
|| data.instance_id !== health?.instance_id
|
|
66
|
+
|| data.pid !== health?.pid) {
|
|
67
|
+
issues.push("health and runtime status do not describe the same adapter instance");
|
|
68
|
+
}
|
|
69
|
+
const capabilities = new Set(Array.isArray(data.capabilities) ? data.capabilities : []);
|
|
70
|
+
if (!capabilities.has(PM_RELAY_CAPABILITY)) issues.push("PM Studio relay capability is missing");
|
|
71
|
+
if (data.profiles?.claude?.mode !== "isolated") issues.push("Claude profile mode is not isolated");
|
|
72
|
+
if (data.profiles?.claude?.profile_current !== true) {
|
|
73
|
+
issues.push("running Claude credentials do not match the current isolated profile");
|
|
74
|
+
}
|
|
75
|
+
if (data.routing?.responses !== "codex") issues.push("/v1/responses is not routed to Codex");
|
|
76
|
+
if (data.routing?.messages !== "claude") issues.push("/v1/messages is not routed to Claude");
|
|
77
|
+
const routes = data.requests?.by_route;
|
|
78
|
+
for (const routeName of PM_RELAY_ROUTE_NAMES) {
|
|
79
|
+
if (!routes || typeof routes !== "object" || !Object.hasOwn(routes, routeName)) {
|
|
80
|
+
issues.push(`runtime status does not expose ${routeName}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...runtimeStatus,
|
|
85
|
+
ok: issues.length === 0,
|
|
86
|
+
issues,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
45
90
|
export async function inspectPmStudioStatus({
|
|
46
91
|
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
47
92
|
home = os.homedir(),
|
|
93
|
+
backupRoot = pmStudioBackupRoot(home),
|
|
48
94
|
recipes = PM_STUDIO_RECIPES,
|
|
49
95
|
operationOverrides = {},
|
|
50
96
|
existsSync = fs.existsSync,
|
|
51
97
|
inspectApp = inspectPmStudioApp,
|
|
98
|
+
verifyPatchRecord = assertPatchedBinaryRecord,
|
|
52
99
|
readClaudeCredentials = readAuthProfileCredentials,
|
|
53
100
|
checkRunningAdapterFn = checkRunningAdapter,
|
|
101
|
+
readAdapterStatusFn = readAdapterStatus,
|
|
54
102
|
} = {}) {
|
|
55
103
|
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
56
104
|
let app;
|
|
@@ -69,9 +117,24 @@ export async function inspectPmStudioStatus({
|
|
|
69
117
|
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
70
118
|
};
|
|
71
119
|
const recipe = selectRecipe(normalized, recipes);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
if (recipe) {
|
|
121
|
+
app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
|
|
122
|
+
if (app.state === "patched") {
|
|
123
|
+
const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
|
|
124
|
+
try {
|
|
125
|
+
verifyPatchRecord({ inspection: app, manifestPath, recipe });
|
|
126
|
+
app.patchRecord = { valid: true, manifestPath, reason: "" };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const reason = safeMessage(error);
|
|
129
|
+
app.patchRecipeMatched = true;
|
|
130
|
+
app.patchRecord = { valid: false, manifestPath, reason };
|
|
131
|
+
app.state = "drift";
|
|
132
|
+
app.issues = [...(Array.isArray(app.issues) ? app.issues : []), reason];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
app = { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
|
|
137
|
+
}
|
|
75
138
|
} catch (error) {
|
|
76
139
|
app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
|
|
77
140
|
}
|
|
@@ -84,16 +147,40 @@ export async function inspectPmStudioStatus({
|
|
|
84
147
|
} catch (error) {
|
|
85
148
|
adapter = { ok: false, error };
|
|
86
149
|
}
|
|
87
|
-
|
|
88
|
-
|
|
150
|
+
let runtime = { ok: false, baseUrl: adapter?.baseUrl, data: null, issues: ["adapter health check failed"] };
|
|
151
|
+
if (adapter?.ok === true) {
|
|
152
|
+
try {
|
|
153
|
+
runtime = inspectAdapterRuntime(adapter, await readAdapterStatusFn({
|
|
154
|
+
host: PM_RELAY_HOST,
|
|
155
|
+
port: PM_RELAY_PORT,
|
|
156
|
+
}));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
runtime = { ok: false, baseUrl: adapter.baseUrl, data: null, issues: [safeMessage(error)] };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const operational = app.state === "patched"
|
|
162
|
+
&& app.patchRecord?.valid === true
|
|
163
|
+
&& claude.valid
|
|
164
|
+
&& adapter?.ok === true
|
|
165
|
+
&& runtime.ok === true;
|
|
166
|
+
return { ok: operational, appPath, app, claude, adapter, runtime };
|
|
89
167
|
}
|
|
90
168
|
|
|
91
169
|
function appStatusLine(result, commandName) {
|
|
92
170
|
const { app } = result;
|
|
93
171
|
const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
|
|
94
|
-
if (app.state === "patched"
|
|
172
|
+
if (app.state === "patched" && app.patchRecord?.valid === true) {
|
|
173
|
+
return status("ok", `PM Studio ${version} is patched and verified against its installed patch record`);
|
|
174
|
+
}
|
|
175
|
+
if (app.state === "patched") {
|
|
176
|
+
const reason = app.patchRecord?.reason || "installed patch record is missing";
|
|
177
|
+
return status("err", `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${reason}`);
|
|
178
|
+
}
|
|
95
179
|
if (app.state === "clean") return status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
|
|
96
180
|
if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
|
|
181
|
+
if (app.state === "drift" && app.patchRecipeMatched) {
|
|
182
|
+
return status("err", `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}`);
|
|
183
|
+
}
|
|
97
184
|
if (app.state === "drift") return status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
|
|
98
185
|
if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
|
|
99
186
|
return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
|
|
@@ -111,14 +198,21 @@ export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
|
|
|
111
198
|
const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
|
|
112
199
|
lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
|
|
113
200
|
}
|
|
114
|
-
if (result.adapter?.ok) {
|
|
115
|
-
lines.push(status("ok", `PM relay
|
|
201
|
+
if (result.adapter?.ok && result.runtime?.ok) {
|
|
202
|
+
lines.push(status("ok", `PM relay and isolated Claude routing are verified at ${result.adapter.baseUrl}`));
|
|
203
|
+
} else if (result.adapter?.ok) {
|
|
204
|
+
const reason = result.runtime?.issues?.join("; ") || "runtime status is unavailable";
|
|
205
|
+
lines.push(status("err", `The adapter is running at ${result.adapter.baseUrl}, but PM relay routing is not ready: ${reason}; stop it and run ${commandName} start`));
|
|
116
206
|
} else if (result.adapter?.incompatible) {
|
|
117
207
|
lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
|
|
118
208
|
} else {
|
|
119
209
|
lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
|
|
120
210
|
}
|
|
121
|
-
|
|
211
|
+
if (result.runtime?.ok) {
|
|
212
|
+
lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
|
|
213
|
+
} else {
|
|
214
|
+
lines.push(status("info", "Expected routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
|
|
215
|
+
}
|
|
122
216
|
return lines.join("\n");
|
|
123
217
|
}
|
|
124
218
|
|
package/src/profile-routing.mjs
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isClaudeCopilotCatalogEntry,
|
|
3
|
+
isClaudeCopilotModel,
|
|
4
|
+
} from "./models.mjs";
|
|
5
|
+
|
|
1
6
|
function catalogData(catalog) {
|
|
2
7
|
if (Array.isArray(catalog)) return catalog;
|
|
3
8
|
return Array.isArray(catalog?.data) ? catalog.data : [];
|
|
4
9
|
}
|
|
5
10
|
|
|
6
|
-
function isClaudeCatalogEntry(model) {
|
|
7
|
-
const id = String(model?.id || "").trim();
|
|
8
|
-
const vendor = String(model?.vendor || "").trim().toLowerCase();
|
|
9
|
-
return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isAllowedClaudeModel(model) {
|
|
13
|
-
return isClaudeCatalogEntry(model)
|
|
14
|
-
&& String(model?.vendor || "").trim().toLowerCase() === "anthropic"
|
|
15
|
-
&& model?.model_picker_enabled !== false
|
|
16
|
-
&& Array.isArray(model?.supported_endpoints)
|
|
17
|
-
&& model.supported_endpoints.includes("/chat/completions");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
11
|
export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
|
|
21
12
|
const isolated = claudeMode === "isolated" || claudeConfigured === true;
|
|
22
13
|
return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
|
|
@@ -44,9 +35,9 @@ export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {})
|
|
|
44
35
|
const known = new Set();
|
|
45
36
|
for (const model of catalogData(catalog)) {
|
|
46
37
|
const id = String(model?.id || "").trim();
|
|
47
|
-
if (!id || !
|
|
38
|
+
if (!id || !isClaudeCopilotCatalogEntry(model)) continue;
|
|
48
39
|
known.add(id);
|
|
49
|
-
if (enabled &&
|
|
40
|
+
if (enabled && isClaudeCopilotModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
50
41
|
}
|
|
51
42
|
cachedCatalog = catalog;
|
|
52
43
|
cachedEnabled = enabled;
|
package/src/profile-runtime.mjs
CHANGED
|
@@ -46,6 +46,15 @@ export function createProfileRuntime({
|
|
|
46
46
|
};
|
|
47
47
|
const credentials = readCredentials();
|
|
48
48
|
const claudeProfile = safeProfileStatus(credentials);
|
|
49
|
+
const claudeCredentialFingerprint = credentials.valid
|
|
50
|
+
? githubTokenFingerprint(credentials.token)
|
|
51
|
+
: "";
|
|
52
|
+
const isClaudeProfileCurrent = () => {
|
|
53
|
+
if (!claudeCredentialFingerprint) return false;
|
|
54
|
+
const current = readCredentials();
|
|
55
|
+
return current.valid === true
|
|
56
|
+
&& githubTokenFingerprint(current.token) === claudeCredentialFingerprint;
|
|
57
|
+
};
|
|
49
58
|
let codexCredentialFingerprint = "";
|
|
50
59
|
try {
|
|
51
60
|
const codexCredentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
@@ -65,6 +74,7 @@ export function createProfileRuntime({
|
|
|
65
74
|
claudeProfile,
|
|
66
75
|
codexCredentialFingerprint,
|
|
67
76
|
claudeCredentialFingerprint: "",
|
|
77
|
+
isClaudeProfileCurrent,
|
|
68
78
|
});
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -86,8 +96,7 @@ export function createProfileRuntime({
|
|
|
86
96
|
claudeMode: "isolated",
|
|
87
97
|
claudeProfile,
|
|
88
98
|
codexCredentialFingerprint,
|
|
89
|
-
claudeCredentialFingerprint
|
|
90
|
-
|
|
91
|
-
: "",
|
|
99
|
+
claudeCredentialFingerprint,
|
|
100
|
+
isClaudeProfileCurrent,
|
|
92
101
|
});
|
|
93
102
|
}
|
package/src/response-history.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { httpError } from "./http-transport.mjs";
|
|
2
|
+
import { clearResponsesToolOutputPartsCache } from "./responses-content.mjs";
|
|
2
3
|
import { loadRuntimeConfig, parsePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
3
4
|
|
|
4
5
|
const DEFAULT_MAX_BYTES = RUNTIME_DEFAULTS.responseHistoryMaxBytes;
|
|
@@ -201,17 +202,26 @@ export function responseHistoryMaterializedBytes(responseId) {
|
|
|
201
202
|
return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
export function materializeResponseHistory(responseId) {
|
|
205
|
+
export function materializeResponseHistory(responseId, { assertActive } = {}) {
|
|
206
|
+
assertActive?.();
|
|
205
207
|
const { chain, rootId } = responseHistoryChain(responseId);
|
|
206
208
|
const items = [];
|
|
207
|
-
for (
|
|
209
|
+
for (let index = 0; index < chain.length; index += 1) {
|
|
210
|
+
if ((index & 63) === 0) assertActive?.();
|
|
211
|
+
const entry = chain[chain.length - index - 1];
|
|
212
|
+
items.push(...entry.inputItems, ...entry.outputItems);
|
|
213
|
+
}
|
|
214
|
+
assertActive?.();
|
|
208
215
|
const materialized = cloneJson(items);
|
|
216
|
+
assertActive?.();
|
|
209
217
|
touchTree(rootId);
|
|
210
218
|
return materialized;
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
export function rememberResponseHistoryNode({ id, parentId, inputItems, outputItems, takeOwnership = false }) {
|
|
214
222
|
if (!id || !Array.isArray(inputItems)) return;
|
|
223
|
+
clearResponsesToolOutputPartsCache(inputItems);
|
|
224
|
+
clearResponsesToolOutputPartsCache(outputItems);
|
|
215
225
|
const bytes = jsonByteLength([inputItems, outputItems]);
|
|
216
226
|
if (bytes > maxBytes) {
|
|
217
227
|
if (histories.has(id)) removeSubtree(id);
|