codex-copilot-dx 0.6.5 → 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 +2 -2
- package/bin/cli.mjs +1 -0
- package/package.json +1 -1
- package/scripts/benchmark.mjs +58 -1
- package/src/adapter.mjs +1 -0
- package/src/cli-auth.mjs +2 -11
- package/src/cli-models.mjs +14 -7
- 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/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
package/README.md
CHANGED
|
@@ -120,8 +120,8 @@ ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
|
120
120
|
ccdx pms setup
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Inspect the installed version, exact patch
|
|
124
|
-
and relay without changing files:
|
|
123
|
+
Inspect the installed version, exact patch manifest plus executable/signing
|
|
124
|
+
integrity, isolated Claude profile, and live relay routing without changing files:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
127
|
ccdx pms status
|
package/bin/cli.mjs
CHANGED
|
@@ -316,6 +316,7 @@ try {
|
|
|
316
316
|
codexModelRegistry: profileModels.codexRegistry,
|
|
317
317
|
claudeModelRegistry: profileModels.claudeRegistry,
|
|
318
318
|
claudeMode: profileRuntime.claudeMode,
|
|
319
|
+
isClaudeProfileCurrent: profileRuntime.isClaudeProfileCurrent,
|
|
319
320
|
showRequestId: CLI.showRequestId,
|
|
320
321
|
upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
|
|
321
322
|
streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
|
package/package.json
CHANGED
package/scripts/benchmark.mjs
CHANGED
|
@@ -23,6 +23,14 @@ function runProbe(source) {
|
|
|
23
23
|
return JSON.parse(result.stdout.trim());
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function median(values) {
|
|
27
|
+
const ordered = [...values].sort((left, right) => left - right);
|
|
28
|
+
const middle = Math.floor(ordered.length / 2);
|
|
29
|
+
return ordered.length % 2 === 0
|
|
30
|
+
? (ordered[middle - 1] + ordered[middle]) / 2
|
|
31
|
+
: ordered[middle];
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function deterministicPixels(byteLength, initialSeed) {
|
|
27
35
|
const pixels = Buffer.alloc(byteLength);
|
|
28
36
|
let seed = initialSeed;
|
|
@@ -154,7 +162,7 @@ function largePayloadProbe(targetMiB, concurrency) {
|
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
function largeTokenCountProbe(mode) {
|
|
157
|
-
|
|
165
|
+
const samples = Array.from({ length: 3 }, () => runProbe(`
|
|
158
166
|
const text = "a ".repeat(2 * 1024 * 1024);
|
|
159
167
|
const started = performance.now();
|
|
160
168
|
let tokens;
|
|
@@ -174,6 +182,51 @@ function largeTokenCountProbe(mode) {
|
|
|
174
182
|
rss_mib: +(memory.rss / 1048576).toFixed(1),
|
|
175
183
|
heap_used_mib: +(memory.heapUsed / 1048576).toFixed(1),
|
|
176
184
|
}));
|
|
185
|
+
`));
|
|
186
|
+
return {
|
|
187
|
+
input_mib: samples[0].input_mib,
|
|
188
|
+
tokens: samples[0].tokens,
|
|
189
|
+
elapsed_ms: +median(samples.map((sample) => sample.elapsed_ms)).toFixed(1),
|
|
190
|
+
rss_mib: +median(samples.map((sample) => sample.rss_mib)).toFixed(1),
|
|
191
|
+
heap_used_mib: +median(samples.map((sample) => sample.heap_used_mib)).toFixed(1),
|
|
192
|
+
samples: samples.length,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toolOutputParseCacheProbe() {
|
|
197
|
+
return runProbe(`
|
|
198
|
+
import { enforceResponsesImageLimit } from "./src/responses-image-limit.mjs";
|
|
199
|
+
import { responsesHistoricalImageStats } from "./src/responses-byte-budget.mjs";
|
|
200
|
+
import { prepareResponsesChatPayload } from "./src/responses-chat-payload.mjs";
|
|
201
|
+
|
|
202
|
+
const item = {
|
|
203
|
+
type: "function_call_output",
|
|
204
|
+
call_id: "call_benchmark",
|
|
205
|
+
output: JSON.stringify([{ type: "input_text", text: "x".repeat(8 * 1024 * 1024) }]),
|
|
206
|
+
};
|
|
207
|
+
const source = item.output;
|
|
208
|
+
const originalParse = JSON.parse;
|
|
209
|
+
let parseCalls = 0;
|
|
210
|
+
JSON.parse = function countedParse(value, ...args) {
|
|
211
|
+
if (value === source) parseCalls += 1;
|
|
212
|
+
return originalParse.call(this, value, ...args);
|
|
213
|
+
};
|
|
214
|
+
const started = performance.now();
|
|
215
|
+
try {
|
|
216
|
+
enforceResponsesImageLimit([item]);
|
|
217
|
+
responsesHistoricalImageStats([item], 1);
|
|
218
|
+
await prepareResponsesChatPayload({
|
|
219
|
+
body: { model: "gpt-4o", input: [item] },
|
|
220
|
+
currentInputStart: 1,
|
|
221
|
+
}, { payloadOptions: { maxBytes: 30 * 1024 * 1024 } });
|
|
222
|
+
} finally {
|
|
223
|
+
JSON.parse = originalParse;
|
|
224
|
+
}
|
|
225
|
+
process.stdout.write(JSON.stringify({
|
|
226
|
+
output_mib: +(Buffer.byteLength(source) / 1048576).toFixed(1),
|
|
227
|
+
parse_calls: parseCalls,
|
|
228
|
+
elapsed_ms: +(performance.now() - started).toFixed(1),
|
|
229
|
+
}));
|
|
177
230
|
`);
|
|
178
231
|
}
|
|
179
232
|
|
|
@@ -373,6 +426,7 @@ if (checkMode) {
|
|
|
373
426
|
proxy: largeTokenCountProbe("proxy"),
|
|
374
427
|
materialized_array: largeTokenCountProbe("array"),
|
|
375
428
|
};
|
|
429
|
+
report.tool_output_parse_cache = toolOutputParseCacheProbe();
|
|
376
430
|
report.large_payload_peak = [
|
|
377
431
|
largePayloadProbe(5, 4),
|
|
378
432
|
largePayloadProbe(30, 1),
|
|
@@ -409,6 +463,9 @@ if (checkMode) {
|
|
|
409
463
|
if (proxy.heap_used_mib > array.heap_used_mib * 0.75) {
|
|
410
464
|
failures.push(`token counter heap regression: proxy=${proxy.heap_used_mib}MiB array=${array.heap_used_mib}MiB`);
|
|
411
465
|
}
|
|
466
|
+
if (report.tool_output_parse_cache.parse_calls !== 1) {
|
|
467
|
+
failures.push(`stringified tool output parsed ${report.tool_output_parse_cache.parse_calls} times instead of once`);
|
|
468
|
+
}
|
|
412
469
|
for (const payload of report.large_payload_peak) {
|
|
413
470
|
if (payload.admission_max_active_mib > payload.admission_budget_mib) {
|
|
414
471
|
failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
|
package/src/adapter.mjs
CHANGED
package/src/cli-auth.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
normalizeGithubIdentity,
|
|
22
22
|
} from "./github-identity.mjs";
|
|
23
23
|
import { saveModelCache } from "./model-cache.mjs";
|
|
24
|
+
import { isClaudeCopilotModel } from "./models.mjs";
|
|
24
25
|
import { profileRouting } from "./profile-routing.mjs";
|
|
25
26
|
import { status } from "./status.mjs";
|
|
26
27
|
|
|
@@ -145,17 +146,7 @@ async function codexIdentity({ home, fetchImpl, signal }) {
|
|
|
145
146
|
function claudeModels(payload) {
|
|
146
147
|
const models = Array.isArray(payload) ? payload : payload?.data;
|
|
147
148
|
if (!Array.isArray(models)) throw new Error("Claude account model catalog contained no model list");
|
|
148
|
-
return models.filter(
|
|
149
|
-
const id = String(model?.id || "").trim();
|
|
150
|
-
const vendor = String(model?.vendor || model?.owned_by || "").trim().toLowerCase();
|
|
151
|
-
const policy = String(model?.policy?.state || "").trim().toLowerCase();
|
|
152
|
-
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
153
|
-
return Boolean(id)
|
|
154
|
-
&& (id.toLowerCase().startsWith("claude-") || vendor === "anthropic")
|
|
155
|
-
&& model?.model_picker_enabled === true
|
|
156
|
-
&& (!policy || policy === "enabled")
|
|
157
|
-
&& (endpoints.includes("/chat/completions") || endpoints.includes("/v1/messages"));
|
|
158
|
-
});
|
|
149
|
+
return models.filter(isClaudeCopilotModel);
|
|
159
150
|
}
|
|
160
151
|
|
|
161
152
|
function catalogModels(payload) {
|
package/src/cli-models.mjs
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
readAuthProfileCredentials,
|
|
7
7
|
} from "./auth-profile.mjs";
|
|
8
8
|
import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
|
|
9
|
+
import {
|
|
10
|
+
isClaudeCopilotCatalogEntry,
|
|
11
|
+
isClaudeCopilotModel,
|
|
12
|
+
} from "./models.mjs";
|
|
9
13
|
import { status } from "./status.mjs";
|
|
10
14
|
|
|
11
15
|
const DEFAULT_MODELS_TIMEOUT_MS = 10000;
|
|
@@ -37,11 +41,15 @@ export function selectableCopilotModels(payload) {
|
|
|
37
41
|
const models = [];
|
|
38
42
|
const seen = new Set();
|
|
39
43
|
for (const model of data) {
|
|
40
|
-
if (model
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if (isClaudeCopilotCatalogEntry(model)) {
|
|
45
|
+
if (!isClaudeCopilotModel(model)) continue;
|
|
46
|
+
} else {
|
|
47
|
+
if (model?.model_picker_enabled !== true) continue;
|
|
48
|
+
const policyState = safeText(model?.policy?.state).toLowerCase();
|
|
49
|
+
if (policyState && policyState !== "enabled") continue;
|
|
50
|
+
const capabilityType = safeText(model?.capabilities?.type).toLowerCase();
|
|
51
|
+
if (capabilityType && capabilityType !== "chat") continue;
|
|
52
|
+
}
|
|
45
53
|
|
|
46
54
|
const id = safeText(model?.id);
|
|
47
55
|
const endpoints = endpointLabels(model);
|
|
@@ -163,8 +171,7 @@ export function formatLiveCopilotModels(catalog, { commandName = "ccdx" } = {})
|
|
|
163
171
|
const profile = catalog?.profile === AUTH_PROFILE_CLAUDE ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX;
|
|
164
172
|
const responses = models.filter((model) => model.endpoints.includes("responses")).length;
|
|
165
173
|
const chat = models.filter((model) => model.endpoints.includes("chat")).length;
|
|
166
|
-
const claude = models.filter(
|
|
167
|
-
|| model.vendor.toLowerCase() === "anthropic").length;
|
|
174
|
+
const claude = models.filter(isClaudeCopilotCatalogEntry).length;
|
|
168
175
|
const lines = [
|
|
169
176
|
`${commandName} models${profile === AUTH_PROFILE_CLAUDE ? " --profile claude" : ""}`,
|
|
170
177
|
status("ok", `Live catalog from ${safeText(catalog?.upstreamHost, "GitHub Copilot")}: ${models.length} selectable of ${Number(catalog?.advertised) || 0} advertised`),
|
package/src/copilot-client.mjs
CHANGED
|
@@ -150,6 +150,7 @@ export function createCopilotClientRuntime({
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
async function responses(reqBody, {
|
|
153
|
+
assertActive,
|
|
153
154
|
signal,
|
|
154
155
|
fetchImpl,
|
|
155
156
|
retryOptions,
|
|
@@ -160,13 +161,14 @@ export function createCopilotClientRuntime({
|
|
|
160
161
|
} = {}) {
|
|
161
162
|
const token = await tokenSession.getToken({ signal });
|
|
162
163
|
const preparedPayload = await prepareResponsesPayload(reqBody, {
|
|
164
|
+
assertActive,
|
|
163
165
|
...payloadOptions,
|
|
164
166
|
currentInputStart,
|
|
165
167
|
profiles: payloadPrepared ? [] : payloadOptions.profiles,
|
|
166
168
|
skipInitialOptimization: payloadPrepared || payloadOptions.skipInitialOptimization,
|
|
167
169
|
signal,
|
|
168
170
|
});
|
|
169
|
-
const finalizedPayload = enforceResponsesPayloadByteBudget(reqBody, preparedPayload);
|
|
171
|
+
const finalizedPayload = enforceResponsesPayloadByteBudget(reqBody, preparedPayload, { assertActive });
|
|
170
172
|
const { bodyText, bodyBytes, stage, adapted } = finalizedPayload;
|
|
171
173
|
const summary = finalizedPayload.stage === preparedPayload.stage
|
|
172
174
|
? preparedPayload.summary
|
package/src/doctor.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { claudeDesktopPaths } from "./claude-desktop-config.mjs";
|
|
|
12
12
|
import { status } from "./status.mjs";
|
|
13
13
|
import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
|
|
14
14
|
import { adapterBaseUrl, checkRunningAdapter } from "./running-adapter.mjs";
|
|
15
|
-
import { CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
15
|
+
import { CODEX_AUTO_REVIEW_MODEL, isClaudeCopilotModel } from "./models.mjs";
|
|
16
16
|
import { loadModelCache } from "./model-cache.mjs";
|
|
17
17
|
|
|
18
18
|
function localGatewayBaseUrl(host, port) {
|
|
@@ -66,7 +66,7 @@ function modelEndpoints(model) {
|
|
|
66
66
|
|
|
67
67
|
export function selectCompatibilityModels(models, { claudeModels = models } = {}) {
|
|
68
68
|
const data = copilotModelData(models).filter((model) => model?.model_picker_enabled !== false);
|
|
69
|
-
const claudeData = copilotModelData(claudeModels)
|
|
69
|
+
const claudeData = copilotModelData(claudeModels);
|
|
70
70
|
const responsesCandidates = data.filter((model) => {
|
|
71
71
|
const id = String(model?.id || "");
|
|
72
72
|
const endpoints = modelEndpoints(model);
|
|
@@ -74,12 +74,7 @@ export function selectCompatibilityModels(models, { claudeModels = models } = {}
|
|
|
74
74
|
});
|
|
75
75
|
const responsesOnly = [...responsesCandidates].reverse()
|
|
76
76
|
.find((model) => !modelEndpoints(model).includes("/chat/completions"));
|
|
77
|
-
const claude = claudeData.find(
|
|
78
|
-
const id = String(model?.id || "");
|
|
79
|
-
const vendor = String(model?.vendor || "").toLowerCase();
|
|
80
|
-
return (id.startsWith("claude-") || vendor === "anthropic")
|
|
81
|
-
&& modelEndpoints(model).includes("/chat/completions");
|
|
82
|
-
});
|
|
77
|
+
const claude = claudeData.find(isClaudeCopilotModel);
|
|
83
78
|
return {
|
|
84
79
|
responsesModel: String((responsesOnly || responsesCandidates[0])?.id || ""),
|
|
85
80
|
claudeModel: String(claude?.id || ""),
|
|
@@ -588,6 +583,12 @@ export async function collectDoctorChecks({
|
|
|
588
583
|
}
|
|
589
584
|
if (!pm.adapter?.ok) {
|
|
590
585
|
checks.push({ kind: "warn", message: "PM Studio relay is not currently available", fix: "ccdx start" });
|
|
586
|
+
} else if (!pm.runtime?.ok) {
|
|
587
|
+
checks.push({
|
|
588
|
+
kind: "warn",
|
|
589
|
+
message: "PM Studio relay is running, but its isolated routing is not ready",
|
|
590
|
+
fix: "stop the running adapter, then run ccdx start",
|
|
591
|
+
});
|
|
591
592
|
}
|
|
592
593
|
} else if (pm.app.state === "clean") {
|
|
593
594
|
checks.push({
|
package/src/http-transport.mjs
CHANGED
|
@@ -47,29 +47,56 @@ function admissionAbortError(signal) {
|
|
|
47
47
|
return error;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label }) {
|
|
50
|
+
function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label, blockBehindExclusive = false }) {
|
|
51
51
|
const queue = [];
|
|
52
52
|
let activeWeight = 0;
|
|
53
|
+
let activeRequestedWeight = 0;
|
|
53
54
|
let activeRequests = 0;
|
|
54
55
|
|
|
55
56
|
const activate = (entry) => {
|
|
56
57
|
entry.cleanup();
|
|
57
58
|
activeWeight += entry.weight;
|
|
59
|
+
activeRequestedWeight += entry.requestedWeight;
|
|
58
60
|
activeRequests += 1;
|
|
59
61
|
let released = false;
|
|
60
|
-
|
|
62
|
+
const release = () => {
|
|
61
63
|
if (released) return;
|
|
62
64
|
released = true;
|
|
63
65
|
activeWeight = Math.max(0, activeWeight - entry.weight);
|
|
66
|
+
activeRequestedWeight = Math.max(0, activeRequestedWeight - entry.requestedWeight);
|
|
64
67
|
activeRequests = Math.max(0, activeRequests - 1);
|
|
65
68
|
drain();
|
|
69
|
+
};
|
|
70
|
+
release.resize = (requestedWeight) => {
|
|
71
|
+
if (released) return 0;
|
|
72
|
+
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
73
|
+
? Math.ceil(requestedWeight)
|
|
74
|
+
: maxWeight;
|
|
75
|
+
const requested = Math.max(1, parsedWeight);
|
|
76
|
+
const weight = Math.min(requested, maxWeight);
|
|
77
|
+
if (weight > entry.weight) {
|
|
78
|
+
throw new RangeError(`${label} admission weight cannot grow`);
|
|
79
|
+
}
|
|
80
|
+
activeWeight = Math.max(0, activeWeight - (entry.weight - weight));
|
|
81
|
+
activeRequestedWeight = Math.max(0, activeRequestedWeight + requested - entry.requestedWeight);
|
|
82
|
+
entry.weight = weight;
|
|
83
|
+
entry.requestedWeight = requested;
|
|
84
|
+
drain();
|
|
85
|
+
return weight;
|
|
86
|
+
};
|
|
87
|
+
Object.defineProperties(release, {
|
|
88
|
+
weight: { get: () => released ? 0 : entry.weight },
|
|
89
|
+
requestedWeight: { get: () => released ? 0 : entry.requestedWeight },
|
|
90
|
+
maxWeight: { value: maxWeight },
|
|
66
91
|
});
|
|
92
|
+
entry.resolve(release);
|
|
67
93
|
};
|
|
68
94
|
|
|
69
95
|
const drain = () => {
|
|
70
96
|
for (let index = 0; index < queue.length;) {
|
|
71
97
|
const entry = queue[index];
|
|
72
98
|
if (activeWeight + entry.weight > maxWeight) {
|
|
99
|
+
if (blockBehindExclusive && entry.weight === maxWeight) break;
|
|
73
100
|
index += 1;
|
|
74
101
|
continue;
|
|
75
102
|
}
|
|
@@ -83,8 +110,12 @@ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, labe
|
|
|
83
110
|
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
84
111
|
? Math.ceil(requestedWeight)
|
|
85
112
|
: maxWeight;
|
|
86
|
-
const
|
|
87
|
-
|
|
113
|
+
const requested = Math.max(1, parsedWeight);
|
|
114
|
+
const weight = Math.min(requested, maxWeight);
|
|
115
|
+
const blockedByExclusive = blockBehindExclusive
|
|
116
|
+
&& queue.some((entry) => entry.weight === maxWeight);
|
|
117
|
+
const mustWait = blockedByExclusive || activeWeight + weight > maxWeight;
|
|
118
|
+
if (queue.length >= maxQueued && mustWait) {
|
|
88
119
|
return Promise.reject(httpError(`${label} queue is full (${maxQueued} waiting)`, 503));
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -92,6 +123,7 @@ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, labe
|
|
|
92
123
|
let timer;
|
|
93
124
|
const entry = {
|
|
94
125
|
weight,
|
|
126
|
+
requestedWeight: requested,
|
|
95
127
|
resolve,
|
|
96
128
|
cleanup: () => {
|
|
97
129
|
if (timer) clearTimeout(timer);
|
|
@@ -120,7 +152,13 @@ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, labe
|
|
|
120
152
|
});
|
|
121
153
|
};
|
|
122
154
|
|
|
123
|
-
acquire.stats = () => ({
|
|
155
|
+
acquire.stats = () => ({
|
|
156
|
+
activeWeight,
|
|
157
|
+
activeRequestedWeight,
|
|
158
|
+
activeRequests,
|
|
159
|
+
queued: queue.length,
|
|
160
|
+
maxWeight,
|
|
161
|
+
});
|
|
124
162
|
return acquire;
|
|
125
163
|
}
|
|
126
164
|
|
|
@@ -143,6 +181,7 @@ export function createRequestAdmission({
|
|
|
143
181
|
maxQueued: queueLimit,
|
|
144
182
|
waitTimeoutMs: timeoutMs,
|
|
145
183
|
label: "Decoded request body",
|
|
184
|
+
blockBehindExclusive: true,
|
|
146
185
|
});
|
|
147
186
|
const acquireResponseHistory = createSupplementalAdmission({
|
|
148
187
|
maxWeight: byteLimit,
|
|
@@ -194,10 +233,30 @@ export function createRequestAdmission({
|
|
|
194
233
|
releaseSupplemental();
|
|
195
234
|
throw admissionAbortError(options?.signal);
|
|
196
235
|
}
|
|
197
|
-
|
|
236
|
+
let supplementalReleased = false;
|
|
237
|
+
const releaseReservation = () => {
|
|
238
|
+
if (supplementalReleased) return;
|
|
239
|
+
supplementalReleased = true;
|
|
240
|
+
supplementalReleases.delete(releaseReservation);
|
|
241
|
+
releaseSupplemental();
|
|
242
|
+
};
|
|
243
|
+
releaseReservation.resize = (nextWeight) => {
|
|
244
|
+
if (supplementalReleased) return 0;
|
|
245
|
+
return releaseSupplemental.resize(nextWeight);
|
|
246
|
+
};
|
|
247
|
+
Object.defineProperties(releaseReservation, {
|
|
248
|
+
weight: { get: () => supplementalReleased ? 0 : releaseSupplemental.weight },
|
|
249
|
+
requestedWeight: {
|
|
250
|
+
get: () => supplementalReleased ? 0 : releaseSupplemental.requestedWeight,
|
|
251
|
+
},
|
|
252
|
+
maxWeight: { value: releaseSupplemental.maxWeight },
|
|
253
|
+
});
|
|
254
|
+
supplementalReleases.add(releaseReservation);
|
|
255
|
+
return releaseReservation;
|
|
198
256
|
};
|
|
199
257
|
release.acquireDecompression = (options) => acquireDecompression(1, options);
|
|
200
258
|
release.reserveDecodedBody = (bytes, options) => reserve(acquireDecodedBody, bytes, options);
|
|
259
|
+
release.reserveDecodedBody.supportsResize = true;
|
|
201
260
|
release.reserveResponseHistory = (bytes, options) => reserve(acquireResponseHistory, bytes, options);
|
|
202
261
|
entry.resolve(release);
|
|
203
262
|
};
|
|
@@ -285,7 +344,8 @@ export function createRequestAdmission({
|
|
|
285
344
|
waitMsMax: counters.waitMsMax,
|
|
286
345
|
decompressionsActive: acquireDecompression.stats().activeRequests,
|
|
287
346
|
decompressionsQueued: acquireDecompression.stats().queued,
|
|
288
|
-
decodedBodyBytes: acquireDecodedBody.stats().
|
|
347
|
+
decodedBodyBytes: acquireDecodedBody.stats().activeRequestedWeight,
|
|
348
|
+
decodedBodyAdmissionBytes: acquireDecodedBody.stats().activeWeight,
|
|
289
349
|
decodedBodiesActive: acquireDecodedBody.stats().activeRequests,
|
|
290
350
|
decodedBodiesQueued: acquireDecodedBody.stats().queued,
|
|
291
351
|
responseHistoryBytes: acquireResponseHistory.stats().activeWeight,
|
|
@@ -445,21 +505,62 @@ export async function readJsonBody(req, {
|
|
|
445
505
|
return parseRequestJson(await readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes, signal));
|
|
446
506
|
}
|
|
447
507
|
const buffer = await readRequestBuffer(req, maxBodyBytes, signal);
|
|
448
|
-
|
|
508
|
+
let releaseDecompression = () => {};
|
|
509
|
+
let decodedReservation;
|
|
510
|
+
let retainDecodedReservation = false;
|
|
449
511
|
try {
|
|
450
512
|
let decoded;
|
|
513
|
+
const supportsResizableReservation = admission?.reserveDecodedBody?.supportsResize === true;
|
|
514
|
+
// Most compressed bodies fit the existing 4x admission estimate. High-ratio bodies retry
|
|
515
|
+
// only after obtaining the pool's exclusive reservation, so a full decoded body never waits
|
|
516
|
+
// outside the decoded-body budget.
|
|
517
|
+
const estimatedDecodedBytes = Math.max(
|
|
518
|
+
1,
|
|
519
|
+
Math.min(buffer.length * COMPRESSED_BODY_WEIGHT_MULTIPLIER, maxDecodedBodyBytes),
|
|
520
|
+
);
|
|
521
|
+
if (supportsResizableReservation) {
|
|
522
|
+
decodedReservation = await admission.reserveDecodedBody(estimatedDecodedBytes, { signal });
|
|
523
|
+
}
|
|
524
|
+
releaseDecompression = await admission?.acquireDecompression?.({ signal }) || (() => {});
|
|
525
|
+
const reservedBytes = decodedReservation?.requestedWeight;
|
|
526
|
+
const reservedAdmissionBytes = decodedReservation?.weight;
|
|
527
|
+
const decodedPoolBytes = decodedReservation?.maxWeight;
|
|
528
|
+
const tentativeDecodeLimit = Number.isFinite(reservedBytes)
|
|
529
|
+
&& Number.isFinite(reservedAdmissionBytes)
|
|
530
|
+
&& Number.isFinite(decodedPoolBytes)
|
|
531
|
+
&& reservedAdmissionBytes < decodedPoolBytes
|
|
532
|
+
? Math.min(reservedBytes, maxDecodedBodyBytes)
|
|
533
|
+
: maxDecodedBodyBytes;
|
|
451
534
|
try {
|
|
452
|
-
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"],
|
|
535
|
+
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], tentativeDecodeLimit);
|
|
453
536
|
} catch (e) {
|
|
454
|
-
if (e?.statusCode)
|
|
455
|
-
|
|
537
|
+
if (e?.statusCode === 413 && tentativeDecodeLimit < maxDecodedBodyBytes && supportsResizableReservation) {
|
|
538
|
+
releaseDecompression();
|
|
539
|
+
releaseDecompression = () => {};
|
|
540
|
+
decodedReservation();
|
|
541
|
+
decodedReservation = await admission.reserveDecodedBody(maxDecodedBodyBytes, { signal });
|
|
542
|
+
releaseDecompression = await admission.acquireDecompression?.({ signal }) || (() => {});
|
|
543
|
+
try {
|
|
544
|
+
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], maxDecodedBodyBytes);
|
|
545
|
+
} catch (retryError) {
|
|
546
|
+
if (retryError?.statusCode) throw retryError;
|
|
547
|
+
throw httpError(`Invalid compressed request body: ${retryError.message}`, 400);
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
if (e?.statusCode) throw e;
|
|
551
|
+
throw httpError(`Invalid compressed request body: ${e.message}`, 400);
|
|
552
|
+
}
|
|
456
553
|
}
|
|
457
554
|
if (signal?.aborted) throw admissionAbortError(signal);
|
|
458
555
|
if (decoded.length > 0) {
|
|
459
|
-
|
|
556
|
+
if (supportsResizableReservation) decodedReservation.resize(decoded.length);
|
|
557
|
+
else decodedReservation = await admission?.reserveDecodedBody?.(decoded.length, { signal });
|
|
460
558
|
}
|
|
461
|
-
|
|
559
|
+
const parsed = parseRequestJson(decoded.toString("utf8"));
|
|
560
|
+
retainDecodedReservation = true;
|
|
561
|
+
return parsed;
|
|
462
562
|
} finally {
|
|
563
|
+
if (!retainDecodedReservation) decodedReservation?.();
|
|
463
564
|
releaseDecompression();
|
|
464
565
|
}
|
|
465
566
|
}
|
|
@@ -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,
|
|
@@ -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
|
}
|