codex-copilot-dx 0.6.18 → 0.6.20
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 +2 -0
- package/bin/cli.mjs +2 -1
- package/package.json +1 -1
- package/src/adapter.mjs +19 -3
- package/src/anthropic-handler.mjs +9 -4
- package/src/anthropic.mjs +10 -3
- package/src/auth-profile.mjs +3 -3
- package/src/auth.mjs +44 -10
- package/src/auto-review-model.mjs +2 -2
- package/src/cli-auth.mjs +98 -25
- package/src/cli-error.mjs +50 -0
- package/src/cli-models.mjs +2 -2
- package/src/cli-options.mjs +10 -8
- package/src/copilot-token-session.mjs +17 -2
- package/src/copilot.mjs +22 -3
- package/src/doctor.mjs +2 -2
- package/src/http-transport.mjs +56 -4
- package/src/lock.mjs +24 -4
- package/src/pm-studio-bundle-content.mjs +165 -0
- package/src/pm-studio-relay.mjs +26 -5
- package/src/pm-studio-setup.mjs +42 -162
- package/src/responses-handler.mjs +7 -2
- package/src/responses-proxy.mjs +6 -1
- package/src/runtime-config.mjs +33 -7
- package/src/token-counter-service.mjs +211 -0
- package/src/token-counter-worker.mjs +16 -0
- package/src/usage.mjs +80 -13
package/src/copilot.mjs
CHANGED
|
@@ -33,9 +33,28 @@ export function parseUpstreamRetryDelayMs(value) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function parseApiBase(data) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const configured = data?.endpoints?.api;
|
|
37
|
+
if (typeof configured !== "string" || !configured.trim()) return DEFAULT_API_BASE;
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(configured.trim());
|
|
41
|
+
} catch {
|
|
42
|
+
throw Object.assign(new Error("Copilot API endpoint is invalid"), {
|
|
43
|
+
code: "CCDX_INVALID_COPILOT_API_ENDPOINT",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (parsed.protocol !== "https:"
|
|
47
|
+
|| !parsed.hostname
|
|
48
|
+
|| parsed.username
|
|
49
|
+
|| parsed.password
|
|
50
|
+
|| parsed.search
|
|
51
|
+
|| parsed.hash) {
|
|
52
|
+
throw Object.assign(new Error("Copilot API endpoint is invalid"), {
|
|
53
|
+
code: "CCDX_INVALID_COPILOT_API_ENDPOINT",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
57
|
+
return `${parsed.origin}${pathname && pathname !== "/" ? pathname : ""}`;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export function responsesEndpointPath() {
|
package/src/doctor.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./auth-profile.mjs";
|
|
11
11
|
import { claudeDesktopPaths } from "./claude-desktop-config.mjs";
|
|
12
12
|
import { status } from "./status.mjs";
|
|
13
|
-
import { buildHeaders,
|
|
13
|
+
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
14
14
|
import { adapterBaseUrl, checkRunningAdapter } from "./running-adapter.mjs";
|
|
15
15
|
import { CODEX_AUTO_REVIEW_MODEL, isClaudeCopilotModel } from "./models.mjs";
|
|
16
16
|
import { loadModelCache } from "./model-cache.mjs";
|
|
@@ -322,7 +322,7 @@ export async function inspectGitHubTokenOnline({
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
const tokenData = validation.copilotTokenData;
|
|
325
|
-
const apiBase = tokenData
|
|
325
|
+
const apiBase = parseApiBase(tokenData);
|
|
326
326
|
const headers = buildHeaders({
|
|
327
327
|
token: tokenData.token,
|
|
328
328
|
version: FALLBACK_VSCODE_VERSION,
|
package/src/http-transport.mjs
CHANGED
|
@@ -20,6 +20,8 @@ const REQUEST_QUEUE_TIMEOUT_MS = HTTP_RUNTIME_CONFIG.requestQueueTimeoutMs;
|
|
|
20
20
|
export const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
|
|
21
21
|
export const MAX_UPSTREAM_MODEL_CATALOG_BYTES = 8 * 1024 * 1024;
|
|
22
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;
|
|
23
25
|
|
|
24
26
|
export function httpError(message, statusCode) {
|
|
25
27
|
const err = new Error(message);
|
|
@@ -57,6 +59,37 @@ async function cancelResponseBody(response) {
|
|
|
57
59
|
try { await response?.body?.cancel?.(); } catch {}
|
|
58
60
|
}
|
|
59
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
|
+
|
|
60
93
|
function responseContentLength(response) {
|
|
61
94
|
const raw = response?.headers?.get?.("content-length")
|
|
62
95
|
?? response?.headers?.["content-length"]
|
|
@@ -68,13 +101,23 @@ function responseContentLength(response) {
|
|
|
68
101
|
export async function readBoundedResponseBuffer(response, {
|
|
69
102
|
maxBytes,
|
|
70
103
|
label = "Upstream response body",
|
|
104
|
+
signal,
|
|
71
105
|
} = {}) {
|
|
72
106
|
const limit = Number.isFinite(maxBytes) && maxBytes >= 0 ? Math.floor(maxBytes) : 0;
|
|
107
|
+
if (signal?.aborted) {
|
|
108
|
+
cancelResponseBody(response);
|
|
109
|
+
throw responseAbortError(signal);
|
|
110
|
+
}
|
|
73
111
|
if (responseContentLength(response) > limit) {
|
|
74
112
|
await cancelResponseBody(response);
|
|
75
113
|
throw upstreamBodyTooLarge(label, limit);
|
|
76
114
|
}
|
|
77
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
|
+
}
|
|
78
121
|
if ((responseContentLength(response) || 0) > 0) throw unreadableUpstreamBody(label);
|
|
79
122
|
return Buffer.alloc(0);
|
|
80
123
|
}
|
|
@@ -84,7 +127,7 @@ export async function readBoundedResponseBuffer(response, {
|
|
|
84
127
|
let total = 0;
|
|
85
128
|
try {
|
|
86
129
|
while (true) {
|
|
87
|
-
const { done, value } = await reader
|
|
130
|
+
const { done, value } = await readResponseChunk(reader, signal);
|
|
88
131
|
if (done) break;
|
|
89
132
|
const chunk = Buffer.from(value);
|
|
90
133
|
total += chunk.length;
|
|
@@ -96,7 +139,11 @@ export async function readBoundedResponseBuffer(response, {
|
|
|
96
139
|
}
|
|
97
140
|
return Buffer.concat(chunks, total);
|
|
98
141
|
} finally {
|
|
99
|
-
|
|
142
|
+
try {
|
|
143
|
+
reader.releaseLock();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!signal?.aborted) throw error;
|
|
146
|
+
}
|
|
100
147
|
}
|
|
101
148
|
}
|
|
102
149
|
|
|
@@ -360,6 +407,9 @@ export function createRequestAdmission({
|
|
|
360
407
|
continue;
|
|
361
408
|
}
|
|
362
409
|
if (activeBytes + entry.weight > byteLimit) {
|
|
410
|
+
// Unknown-length requests reserve the whole budget. Once one is waiting,
|
|
411
|
+
// do not let later arrivals keep extending its wait indefinitely.
|
|
412
|
+
if (entry.weight === byteLimit) break;
|
|
363
413
|
index += 1;
|
|
364
414
|
continue;
|
|
365
415
|
}
|
|
@@ -375,11 +425,13 @@ export function createRequestAdmission({
|
|
|
375
425
|
return Promise.reject(admissionAbortError(signal));
|
|
376
426
|
}
|
|
377
427
|
const weight = requestAdmissionWeight(req, byteLimit);
|
|
378
|
-
|
|
428
|
+
const blockedByExclusive = queue.some((entry) => !entry.cancelled && entry.weight === byteLimit);
|
|
429
|
+
const mustWait = blockedByExclusive || activeBytes + weight > byteLimit;
|
|
430
|
+
if (queue.length >= queueLimit && mustWait) {
|
|
379
431
|
counters.rejected += 1;
|
|
380
432
|
return Promise.reject(httpError(`Request queue is full (${queueLimit} waiting)`, 503));
|
|
381
433
|
}
|
|
382
|
-
if (
|
|
434
|
+
if (mustWait) counters.queued += 1;
|
|
383
435
|
|
|
384
436
|
return new Promise((resolve, reject) => {
|
|
385
437
|
let timer;
|
package/src/lock.mjs
CHANGED
|
@@ -149,8 +149,12 @@ function maybeRemoveAbandonedGuard(guardPath, staleMs, nowMs, io, processAlive)
|
|
|
149
149
|
restoreQuarantinedLock(guardPath, quarantinePath, io);
|
|
150
150
|
return false;
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
try {
|
|
153
|
+
io.unlinkSync(quarantinePath);
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return recoverQuarantineUnlinkFailure(guardPath, quarantinePath, snapshot, io);
|
|
157
|
+
}
|
|
154
158
|
} catch (error) {
|
|
155
159
|
return error?.code === "ENOENT";
|
|
156
160
|
}
|
|
@@ -167,6 +171,18 @@ function restoreQuarantinedLock(lockPath, quarantinePath, io) {
|
|
|
167
171
|
} catch {}
|
|
168
172
|
}
|
|
169
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);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return error?.code === "ENOENT";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
170
186
|
function maybeRemoveStaleLock(lockPath, staleMs, nowMs, io, processAlive) {
|
|
171
187
|
if (!Number.isFinite(staleMs) || staleMs <= 0) return false;
|
|
172
188
|
try {
|
|
@@ -192,8 +208,12 @@ function maybeRemoveStaleLock(lockPath, staleMs, nowMs, io, processAlive) {
|
|
|
192
208
|
restoreQuarantinedLock(lockPath, quarantinePath, io);
|
|
193
209
|
return false;
|
|
194
210
|
}
|
|
195
|
-
|
|
196
|
-
|
|
211
|
+
try {
|
|
212
|
+
io.unlinkSync(quarantinePath);
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return recoverQuarantineUnlinkFailure(lockPath, quarantinePath, current, io);
|
|
216
|
+
}
|
|
197
217
|
} finally {
|
|
198
218
|
removeOwnedLock(guard.guardPath, guard.owner, io);
|
|
199
219
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { sha256Hex } from "./pm-studio-asar.mjs";
|
|
5
|
+
|
|
6
|
+
const BUNDLE_CONTENT_SCHEME = "ccdx-bundle-content-v2";
|
|
7
|
+
|
|
8
|
+
function setupError(code, message, details) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.code = code;
|
|
11
|
+
if (details !== undefined) error.details = details;
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hashRegularFile(filePath, expected, readBuffer) {
|
|
16
|
+
const hash = createHash("sha256");
|
|
17
|
+
const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
18
|
+
const buffer = readBuffer || Buffer.allocUnsafe(1024 * 1024);
|
|
19
|
+
try {
|
|
20
|
+
const before = fs.fstatSync(file, { bigint: true });
|
|
21
|
+
if (before.dev !== BigInt(expected.dev)
|
|
22
|
+
|| before.ino !== BigInt(expected.ino)
|
|
23
|
+
|| before.size !== BigInt(expected.size)) {
|
|
24
|
+
throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while reading ${filePath}`);
|
|
25
|
+
}
|
|
26
|
+
for (;;) {
|
|
27
|
+
const length = fs.readSync(file, buffer, 0, buffer.length, null);
|
|
28
|
+
if (length === 0) break;
|
|
29
|
+
hash.update(buffer.subarray(0, length));
|
|
30
|
+
}
|
|
31
|
+
const after = fs.fstatSync(file, { bigint: true });
|
|
32
|
+
if (before.dev !== after.dev
|
|
33
|
+
|| before.ino !== after.ino
|
|
34
|
+
|| before.size !== after.size
|
|
35
|
+
|| before.mtimeNs !== after.mtimeNs
|
|
36
|
+
|| before.ctimeNs !== after.ctimeNs) {
|
|
37
|
+
throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while hashing ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
fs.closeSync(file);
|
|
41
|
+
}
|
|
42
|
+
return hash.digest("hex");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseXattrHex(output, root, ignoredXattrs) {
|
|
46
|
+
const records = [];
|
|
47
|
+
const ignored = new Set(ignoredXattrs);
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
let current = null;
|
|
50
|
+
const flush = () => {
|
|
51
|
+
if (!current) return;
|
|
52
|
+
if (!ignored.has(current.name)) {
|
|
53
|
+
const key = `${current.path}\0${current.name}`;
|
|
54
|
+
if (seen.has(key)) throw setupError("PM_STUDIO_XATTR_INVALID", `Duplicate extended attribute ${current.name}`);
|
|
55
|
+
seen.add(key);
|
|
56
|
+
const value = Buffer.from(current.hex.join(""), "hex");
|
|
57
|
+
records.push(["xattr", current.path, current.name, value.length, sha256Hex(value)]);
|
|
58
|
+
}
|
|
59
|
+
current = null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
63
|
+
if (!line) continue;
|
|
64
|
+
const separator = line.lastIndexOf(": ");
|
|
65
|
+
if ((line === `${root}:` || line.startsWith(`${root}: `) || line.startsWith(`${root}${path.sep}`))
|
|
66
|
+
&& separator >= root.length && line.endsWith(":")) {
|
|
67
|
+
flush();
|
|
68
|
+
const target = line.slice(0, separator);
|
|
69
|
+
const relative = target === root ? "." : path.relative(root, target).split(path.sep).join("/");
|
|
70
|
+
if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) {
|
|
71
|
+
throw setupError("PM_STUDIO_XATTR_INVALID", `Extended attribute path escapes PM Studio: ${target}`);
|
|
72
|
+
}
|
|
73
|
+
current = { path: relative, name: line.slice(separator + 2, -1), hex: [] };
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!current) throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attributes");
|
|
77
|
+
const data = line.includes(" |") ? line.slice(0, line.indexOf(" |")) : line;
|
|
78
|
+
const tokens = data.trim().split(/\s+/);
|
|
79
|
+
if (!/^[0-9a-f]{8}$/i.test(tokens[0]) || tokens.slice(1).some((token) => !/^[0-9a-f]{2}$/i.test(token))) {
|
|
80
|
+
throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attribute bytes");
|
|
81
|
+
}
|
|
82
|
+
current.hex.push(...tokens.slice(1));
|
|
83
|
+
}
|
|
84
|
+
flush();
|
|
85
|
+
return records.sort((left, right) => Buffer.compare(
|
|
86
|
+
Buffer.from(`${left[1]}\0${left[2]}`),
|
|
87
|
+
Buffer.from(`${right[1]}\0${right[2]}`),
|
|
88
|
+
));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function inspectBundleContent(appPath, {
|
|
92
|
+
xattrOutput = "",
|
|
93
|
+
ignoredXattrs = [],
|
|
94
|
+
readBuffer,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const root = path.resolve(appPath);
|
|
97
|
+
const rootStat = fs.lstatSync(root);
|
|
98
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
99
|
+
throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
|
|
100
|
+
}
|
|
101
|
+
const records = [];
|
|
102
|
+
const normalizedPaths = new Map();
|
|
103
|
+
let regularFileCount = 0;
|
|
104
|
+
let regularBytes = 0;
|
|
105
|
+
let symlinkCount = 0;
|
|
106
|
+
|
|
107
|
+
const rememberPath = (relative) => {
|
|
108
|
+
const normalized = relative.normalize("NFC");
|
|
109
|
+
const existing = normalizedPaths.get(normalized);
|
|
110
|
+
if (existing && existing !== relative) {
|
|
111
|
+
throw setupError("PM_STUDIO_BUNDLE_PATH_COLLISION", `PM Studio contains colliding paths: ${existing} and ${relative}`);
|
|
112
|
+
}
|
|
113
|
+
normalizedPaths.set(normalized, relative);
|
|
114
|
+
};
|
|
115
|
+
rememberPath(".");
|
|
116
|
+
records.push(["directory", ".", rootStat.mode & 0o7777]);
|
|
117
|
+
const visit = (directory) => {
|
|
118
|
+
const names = fs.readdirSync(directory).sort((left, right) =>
|
|
119
|
+
Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
const fullPath = path.join(directory, name);
|
|
122
|
+
const stat = fs.lstatSync(fullPath);
|
|
123
|
+
const relative = path.relative(root, fullPath).split(path.sep).join("/");
|
|
124
|
+
const mode = stat.mode & 0o7777;
|
|
125
|
+
rememberPath(relative);
|
|
126
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
127
|
+
records.push(["directory", relative, mode]);
|
|
128
|
+
visit(fullPath);
|
|
129
|
+
} else if (stat.isSymbolicLink()) {
|
|
130
|
+
const target = fs.readlinkSync(fullPath);
|
|
131
|
+
const resolved = path.resolve(path.dirname(fullPath), target);
|
|
132
|
+
if (path.isAbsolute(target)
|
|
133
|
+
|| (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
|
|
134
|
+
|| !fs.existsSync(resolved)) {
|
|
135
|
+
throw setupError("PM_STUDIO_BUNDLE_SYMLINK_INVALID", `PM Studio contains an unsafe symlink: ${relative}`);
|
|
136
|
+
}
|
|
137
|
+
symlinkCount += 1;
|
|
138
|
+
records.push(["symlink", relative, mode, target]);
|
|
139
|
+
} else if (stat.isFile()) {
|
|
140
|
+
if (stat.nlink !== 1) {
|
|
141
|
+
throw setupError("PM_STUDIO_BUNDLE_HARDLINK_INVALID", `PM Studio contains a hard-linked file: ${relative}`);
|
|
142
|
+
}
|
|
143
|
+
regularFileCount += 1;
|
|
144
|
+
regularBytes += stat.size;
|
|
145
|
+
records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat, readBuffer)]);
|
|
146
|
+
} else {
|
|
147
|
+
throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
visit(root);
|
|
152
|
+
|
|
153
|
+
const xattrRecords = parseXattrHex(xattrOutput, root, ignoredXattrs);
|
|
154
|
+
const hash = createHash("sha256");
|
|
155
|
+
for (const record of [...records, ...xattrRecords]) hash.update(`${JSON.stringify(record)}\n`);
|
|
156
|
+
return {
|
|
157
|
+
scheme: BUNDLE_CONTENT_SCHEME,
|
|
158
|
+
sha256: hash.digest("hex"),
|
|
159
|
+
entryCount: records.length,
|
|
160
|
+
regularFileCount,
|
|
161
|
+
regularBytes,
|
|
162
|
+
symlinkCount,
|
|
163
|
+
xattrCount: xattrRecords.length,
|
|
164
|
+
};
|
|
165
|
+
}
|
package/src/pm-studio-relay.mjs
CHANGED
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
createRequestAdmission,
|
|
6
6
|
httpError,
|
|
7
7
|
isAbortLikeError,
|
|
8
|
+
MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
9
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
10
|
+
MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
11
|
+
readBoundedResponseBuffer,
|
|
8
12
|
readJsonBody,
|
|
9
13
|
sendJsonError,
|
|
10
14
|
writeOrDrain,
|
|
@@ -150,12 +154,12 @@ function catalogData(catalog) {
|
|
|
150
154
|
return Array.isArray(catalog?.data) ? catalog.data : null;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
async function responseSnapshot(response) {
|
|
157
|
+
async function responseSnapshot(response, { maxBytes, label, signal }) {
|
|
154
158
|
return {
|
|
155
159
|
status: response.status,
|
|
156
160
|
ok: response.ok,
|
|
157
161
|
headers: new Headers(response.headers),
|
|
158
|
-
body:
|
|
162
|
+
body: await readBoundedResponseBuffer(response, { maxBytes, label, signal }),
|
|
159
163
|
};
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -209,7 +213,15 @@ function mergeModelCatalog(snapshot, modelRouter) {
|
|
|
209
213
|
async function relayUpstreamResponse(response, reqBody, res, abort, streamIdleTimeoutMs) {
|
|
210
214
|
const stream = reqBody?.stream === true;
|
|
211
215
|
if (!stream || !response.ok) {
|
|
212
|
-
const snapshot = await responseSnapshot(response
|
|
216
|
+
const snapshot = await responseSnapshot(response, response.ok ? {
|
|
217
|
+
maxBytes: MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
218
|
+
label: "PM Studio Claude success body",
|
|
219
|
+
signal: abort.signal,
|
|
220
|
+
} : {
|
|
221
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
222
|
+
label: "PM Studio Claude error body",
|
|
223
|
+
signal: abort.signal,
|
|
224
|
+
});
|
|
213
225
|
sendSnapshot(res, snapshot);
|
|
214
226
|
return;
|
|
215
227
|
}
|
|
@@ -345,9 +357,18 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
345
357
|
}
|
|
346
358
|
activeModelRequests += 1;
|
|
347
359
|
try {
|
|
348
|
-
|
|
360
|
+
const response = await fetchEnterprise(
|
|
349
361
|
"/models", "GET", inboundHeaders, token, undefined, signal,
|
|
350
|
-
)
|
|
362
|
+
);
|
|
363
|
+
return await responseSnapshot(response, response.ok ? {
|
|
364
|
+
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
365
|
+
label: "PM Studio model catalog",
|
|
366
|
+
signal,
|
|
367
|
+
} : {
|
|
368
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
369
|
+
label: "PM Studio model catalog error body",
|
|
370
|
+
signal,
|
|
371
|
+
});
|
|
351
372
|
} finally {
|
|
352
373
|
activeModelRequests -= 1;
|
|
353
374
|
}
|