codex-copilot-dx 0.4.43 → 0.5.0
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 +31 -13
- package/bin/cli.mjs +31 -7
- package/bin/codex-copilot-dx.mjs +2 -0
- package/package.json +3 -2
- package/src/adapter.mjs +16 -8
- package/src/anthropic-handler.mjs +13 -5
- package/src/anthropic.mjs +40 -5
- package/src/auth.mjs +2 -2
- package/src/bounded-result-cache.mjs +180 -0
- package/src/claude-config.mjs +1 -1
- package/src/cli-options.mjs +30 -6
- package/src/cli-status.mjs +122 -0
- package/src/copilot.mjs +11 -4
- package/src/doctor.mjs +11 -2
- package/src/http-transport.mjs +4 -1
- package/src/image-optimization.mjs +74 -65
- package/src/observability.mjs +4 -3
- package/src/responses-bridge.mjs +46 -10
- package/src/responses-chat-payload.mjs +2 -1
- package/src/responses-compaction.mjs +113 -0
- package/src/responses-handler.mjs +28 -14
- package/src/responses-proxy.mjs +123 -23
- package/src/running-adapter.mjs +1 -0
- package/src/stream-contract.mjs +102 -0
- package/src/stream-errors.mjs +3 -1
- package/src/stream-performance.mjs +177 -0
- package/src/upstream-headers.mjs +67 -0
package/src/cli-options.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
|
|
2
2
|
const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
|
|
3
3
|
const START_OPTIONS = new Set(["--configure-claude-desktop", "--show-request-id"]);
|
|
4
|
+
const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
|
|
4
5
|
|
|
5
6
|
function unexpectedArgs(args) {
|
|
6
7
|
throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
|
|
@@ -17,6 +18,10 @@ export function parseCliArgs(args = []) {
|
|
|
17
18
|
if (rest.length) unexpectedArgs(rest);
|
|
18
19
|
return { command: "version", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
19
20
|
}
|
|
21
|
+
if (command === "status" || command === "--status") {
|
|
22
|
+
if (rest.length) unexpectedArgs(rest);
|
|
23
|
+
return { command: "status", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
24
|
+
}
|
|
20
25
|
if (command === "usage") {
|
|
21
26
|
if (rest.length) unexpectedArgs(rest);
|
|
22
27
|
return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
@@ -80,11 +85,30 @@ export function parseRuntimeOptions(env = process.env) {
|
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
export function
|
|
88
|
+
export function parseAdapterProbeOptions(env = process.env) {
|
|
89
|
+
return {
|
|
90
|
+
adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
|
|
91
|
+
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
92
|
+
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1 }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function cliCommandName(executable = process.argv[1]) {
|
|
97
|
+
const name = String(executable || "").split(/[\\/]/).pop();
|
|
98
|
+
if (name === "codex-copilot-dx.mjs") return "codex-copilot-dx";
|
|
99
|
+
return CLI_COMMANDS.has(name) ? name : "ccdx";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function cliHelp(commandName = "ccdx") {
|
|
103
|
+
const name = CLI_COMMANDS.has(commandName) ? commandName : "ccdx";
|
|
104
|
+
const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
|
|
84
105
|
return `Usage:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
${name} [--configure-claude-desktop] [--show-request-id]
|
|
107
|
+
${name} doctor [--online] [--compat]
|
|
108
|
+
${name} status
|
|
109
|
+
${name} usage
|
|
110
|
+
${name} --version
|
|
111
|
+
${name} --help
|
|
112
|
+
|
|
113
|
+
Equivalent command: ${alias}`;
|
|
90
114
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ADAPTER_STATUS_PATH, adapterBaseUrl } from "./running-adapter.mjs";
|
|
2
|
+
import { status } from "./status.mjs";
|
|
3
|
+
|
|
4
|
+
function finiteNumber(value) {
|
|
5
|
+
const number = Number(value);
|
|
6
|
+
return Number.isFinite(number) ? number : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function count(value) {
|
|
10
|
+
const number = finiteNumber(value);
|
|
11
|
+
return number === null ? "n/a" : String(Math.max(0, Math.round(number)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function duration(value) {
|
|
15
|
+
const milliseconds = finiteNumber(value);
|
|
16
|
+
if (milliseconds === null) return "n/a";
|
|
17
|
+
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
18
|
+
const days = Math.floor(seconds / 86400);
|
|
19
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
20
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
21
|
+
const remainder = seconds % 60;
|
|
22
|
+
if (days) return `${days}d ${hours}h`;
|
|
23
|
+
if (hours) return `${hours}h ${minutes}m`;
|
|
24
|
+
if (minutes) return `${minutes}m ${remainder}s`;
|
|
25
|
+
return `${remainder}s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function latency(value) {
|
|
29
|
+
const milliseconds = finiteNumber(value);
|
|
30
|
+
if (milliseconds === null) return "n/a";
|
|
31
|
+
if (milliseconds >= 1000) return `${(milliseconds / 1000).toFixed(2)}s`;
|
|
32
|
+
return `${milliseconds.toFixed(1)}ms`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mebibytes(value) {
|
|
36
|
+
const bytes = finiteNumber(value);
|
|
37
|
+
return bytes === null ? "n/a" : `${(bytes / (1024 * 1024)).toFixed(1)}MiB`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tpot(metric) {
|
|
41
|
+
const average = finiteNumber(metric?.avg);
|
|
42
|
+
if (average === null) return "n/a";
|
|
43
|
+
const milliseconds = metric?.unit === "us" ? average / 1000 : average;
|
|
44
|
+
return `${milliseconds.toFixed(2)}ms/token`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cacheHitRate(cache = {}) {
|
|
48
|
+
const hits = finiteNumber(cache.cache_hits);
|
|
49
|
+
const misses = finiteNumber(cache.cache_misses);
|
|
50
|
+
if (hits === null || misses === null || hits + misses <= 0) return "n/a";
|
|
51
|
+
return `${((hits / (hits + misses)) * 100).toFixed(1)}%`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readAdapterStatus({
|
|
55
|
+
host = "127.0.0.1",
|
|
56
|
+
port = 2026,
|
|
57
|
+
timeoutMs = 500,
|
|
58
|
+
fetchImpl = fetch,
|
|
59
|
+
} = {}) {
|
|
60
|
+
const baseUrl = adapterBaseUrl(host, port);
|
|
61
|
+
const ctrl = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetchImpl(`${baseUrl}${ADAPTER_STATUS_PATH}`, {
|
|
65
|
+
headers: { Accept: "application/json" },
|
|
66
|
+
redirect: "error",
|
|
67
|
+
signal: ctrl.signal,
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) throw new Error(`returned HTTP ${response.status}`);
|
|
70
|
+
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
data = await response.json();
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("returned invalid JSON");
|
|
76
|
+
}
|
|
77
|
+
if (data?.ok !== true
|
|
78
|
+
|| data?.name !== "codex-copilot-dx"
|
|
79
|
+
|| typeof data.version !== "string"
|
|
80
|
+
|| !Number.isInteger(data.pid)) {
|
|
81
|
+
throw new Error("returned an incompatible status payload");
|
|
82
|
+
}
|
|
83
|
+
return { baseUrl, data };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const reason = ctrl.signal.aborted
|
|
86
|
+
? `timed out after ${timeoutMs}ms`
|
|
87
|
+
: error?.cause?.code === "ECONNREFUSED"
|
|
88
|
+
? "adapter is not running"
|
|
89
|
+
: error?.message || String(error);
|
|
90
|
+
throw new Error(`Could not read adapter status at ${baseUrl}: ${reason}`);
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", cliVersion = data.version } = {}) {
|
|
97
|
+
const requests = data.requests || {};
|
|
98
|
+
const performance = data.stream_performance?.by_route?.responses || {};
|
|
99
|
+
const admission = data.admission || {};
|
|
100
|
+
const history = data.response_history || {};
|
|
101
|
+
const images = data.image_optimization || {};
|
|
102
|
+
const models = data.models || {};
|
|
103
|
+
const copilot = data.copilot || {};
|
|
104
|
+
const limits = data.limits || {};
|
|
105
|
+
const processStats = data.process || {};
|
|
106
|
+
const adapterLine = data.version === cliVersion
|
|
107
|
+
? status("ok", `Adapter ${data.version} is running at ${baseUrl} (PID ${data.pid}, uptime ${duration(data.uptime_ms)})`)
|
|
108
|
+
: status("warn", `Adapter ${data.version} is running at ${baseUrl}, but this CLI is ${cliVersion}; stop the running adapter before switching versions`);
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
`${commandName} status`,
|
|
112
|
+
adapterLine,
|
|
113
|
+
status("info", `Requests: ${count(requests.completed)}/${count(requests.total)} completed, ${count(requests.active)} active, ${count(requests.status_4xx)} 4xx, ${count(requests.status_5xx)} 5xx, ${count(requests.aborted)} aborted`),
|
|
114
|
+
status("info", `Responses stream: TTFT avg ${latency(performance.ttft_ms?.avg)} (${count(performance.ttft_ms?.samples)} samples), TPOT avg ${tpot(performance.tpot_us)}`),
|
|
115
|
+
status("info", `Admission: ${count(admission.activeRequests)} active, ${count(admission.queued)} queued, ${count(admission.rejected)} rejected, ${count(admission.timedOut)} timed out, wait avg ${latency(admission.waitMsAvg)}`),
|
|
116
|
+
status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
|
|
117
|
+
status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
|
|
118
|
+
status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
|
|
119
|
+
status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`),
|
|
120
|
+
status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`),
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
package/src/copilot.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from "./github-identity.mjs";
|
|
20
20
|
import { prepareResponsesPayload, summarizeReqBody } from "./image-optimization.mjs";
|
|
21
21
|
import { enforceResponsesPayloadByteBudget } from "./responses-byte-budget.mjs";
|
|
22
|
+
import { withChatStreamUsage } from "./stream-contract.mjs";
|
|
23
|
+
import { markUpstreamStarted } from "./stream-performance.mjs";
|
|
22
24
|
|
|
23
25
|
export {
|
|
24
26
|
optimizeImageDataUrl,
|
|
@@ -317,7 +319,7 @@ export function copilotRuntimeStatus(now = Date.now()) {
|
|
|
317
319
|
function getGithubToken({ home = os.homedir() } = {}) {
|
|
318
320
|
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
319
321
|
if (!fs.existsSync(GITHUB_TOKEN_PATH)) {
|
|
320
|
-
throw new Error("GitHub token not found. Run
|
|
322
|
+
throw new Error("GitHub token not found. Run ccdx again to log in.");
|
|
321
323
|
}
|
|
322
324
|
const token = fs.readFileSync(GITHUB_TOKEN_PATH, "utf-8").trim();
|
|
323
325
|
if (!token) throw new Error(githubReauthMessage("GitHub token file is empty."));
|
|
@@ -343,7 +345,7 @@ function cacheCopilotTokenData(data, {
|
|
|
343
345
|
const tokenChanged = Boolean(copilotTokenGithubFingerprint && fingerprint !== copilotTokenGithubFingerprint);
|
|
344
346
|
if (!allowAccountSwitch && copilotTokenGithubIdentity && identity
|
|
345
347
|
&& !githubIdentitiesEqual(copilotTokenGithubIdentity, identity)) {
|
|
346
|
-
throw new Error(`Refusing to switch GitHub Copilot account from ${githubIdentityLabel(copilotTokenGithubIdentity)} to ${githubIdentityLabel(identity)} while the adapter is running. Restart
|
|
348
|
+
throw new Error(`Refusing to switch GitHub Copilot account from ${githubIdentityLabel(copilotTokenGithubIdentity)} to ${githubIdentityLabel(identity)} while the adapter is running. Restart ccdx to switch accounts intentionally.`);
|
|
347
349
|
}
|
|
348
350
|
if (!allowAccountSwitch && copilotTokenGithubIdentity && tokenChanged && !identity) {
|
|
349
351
|
const error = new Error("The saved GitHub token changed, but its account could not be verified. Keeping the running adapter bound to the existing GitHub account.");
|
|
@@ -508,8 +510,11 @@ export async function chatCompletions(chatReq, {
|
|
|
508
510
|
bodyText,
|
|
509
511
|
} = {}) {
|
|
510
512
|
const token = await getCopilotToken({ signal });
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
+
const upstreamReq = chatReq.stream === true ? withChatStreamUsage(chatReq) : chatReq;
|
|
514
|
+
const messages = upstreamReq.messages || [];
|
|
515
|
+
const serializedBody = typeof bodyText === "string" && upstreamReq === chatReq
|
|
516
|
+
? bodyText
|
|
517
|
+
: JSON.stringify(upstreamReq);
|
|
513
518
|
const headers = buildHeaders({
|
|
514
519
|
token,
|
|
515
520
|
version: getVSCodeVersion(),
|
|
@@ -517,6 +522,7 @@ export async function chatCompletions(chatReq, {
|
|
|
517
522
|
vision: computeVision(messages),
|
|
518
523
|
});
|
|
519
524
|
headers["Content-Length"] = String(Buffer.byteLength(serializedBody));
|
|
525
|
+
if (upstreamReq.stream === true) markUpstreamStarted();
|
|
520
526
|
return fetchCopilotUpstream(`${getApiBase()}/chat/completions`, {
|
|
521
527
|
method: "POST",
|
|
522
528
|
headers,
|
|
@@ -624,6 +630,7 @@ export async function responses(reqBody, {
|
|
|
624
630
|
headers["Accept"] = reqBody.stream ? "text/event-stream" : "application/json";
|
|
625
631
|
|
|
626
632
|
try {
|
|
633
|
+
if (reqBody.stream === true) markUpstreamStarted();
|
|
627
634
|
return await fetchCopilotUpstream(`${getApiBase()}${responsesEndpointPath()}`, {
|
|
628
635
|
method: "POST",
|
|
629
636
|
headers,
|
package/src/doctor.mjs
CHANGED
|
@@ -124,6 +124,15 @@ function parseResponseObject(text) {
|
|
|
124
124
|
return response;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function parseCompactionResponse(text) {
|
|
128
|
+
const response = parseResponseObject(text);
|
|
129
|
+
if (response.object !== "response.compaction"
|
|
130
|
+
|| !response.output.some((item) => item?.type === "compaction")) {
|
|
131
|
+
throw new Error("response body is missing valid compaction state");
|
|
132
|
+
}
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
export async function inspectAdapterCompatibility({
|
|
128
137
|
host = "127.0.0.1",
|
|
129
138
|
port = 2026,
|
|
@@ -184,7 +193,7 @@ export async function inspectAdapterCompatibility({
|
|
|
184
193
|
stream: false,
|
|
185
194
|
input: "Compact this short context.",
|
|
186
195
|
}, timeoutMs);
|
|
187
|
-
|
|
196
|
+
parseCompactionResponse(text);
|
|
188
197
|
});
|
|
189
198
|
|
|
190
199
|
if (!claudeModel) {
|
|
@@ -446,7 +455,7 @@ export async function collectDoctorChecks({
|
|
|
446
455
|
export async function runDoctor(options = {}) {
|
|
447
456
|
const log = options.log || console.log;
|
|
448
457
|
const flags = [options.online ? "--online" : "", options.compat ? "--compat" : ""].filter(Boolean);
|
|
449
|
-
log(
|
|
458
|
+
log(`${options.commandName || "ccdx"} doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
450
459
|
const checks = await collectDoctorChecks(options);
|
|
451
460
|
for (const check of checks) log(status(check.kind, check.message));
|
|
452
461
|
return checks;
|
package/src/http-transport.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { promisify } from "node:util";
|
|
|
2
2
|
import * as zlib from "node:zlib";
|
|
3
3
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
|
+
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
5
6
|
|
|
6
7
|
const COMPRESSED_BODY_WEIGHT_MULTIPLIER = 4;
|
|
7
8
|
const gunzipAsync = promisify(zlib.gunzip);
|
|
@@ -296,7 +297,9 @@ export function sendJsonError(res, err, fallbackStatus = 400) {
|
|
|
296
297
|
|
|
297
298
|
export function sendUpstreamError(res, response, text) {
|
|
298
299
|
if (!res.headersSent) {
|
|
299
|
-
res.writeHead(response.status || 502,
|
|
300
|
+
res.writeHead(response.status || 502, safeUpstreamResponseHeaders(response.headers, {
|
|
301
|
+
defaultContentType: "application/json",
|
|
302
|
+
}));
|
|
300
303
|
}
|
|
301
304
|
res.end(text || JSON.stringify({ error: "Upstream request failed" }));
|
|
302
305
|
}
|
|
@@ -4,17 +4,19 @@ import {
|
|
|
4
4
|
readResponsesImagePart,
|
|
5
5
|
readResponsesToolOutputParts,
|
|
6
6
|
} from "./responses-content.mjs";
|
|
7
|
+
import { createBoundedResultCache } from "./bounded-result-cache.mjs";
|
|
7
8
|
import { status } from "./status.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_IMG_CONCURRENCY = 2;
|
|
10
11
|
const IMG_MAX_CONCURRENCY = 12;
|
|
11
12
|
const DEFAULT_IMG_MAX_INPUT_PIXELS = 40 * 1000 * 1000;
|
|
13
|
+
const DEFAULT_IMG_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
12
14
|
const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
|
|
13
|
-
const MAX_OPTIMIZED_IMAGE_DIGESTS = 4096;
|
|
14
15
|
const IMG_MAX_DIM = positiveInt(process.env.CCDX_IMG_MAX_DIM, 2048);
|
|
15
16
|
const IMG_QUALITY = positiveInt(process.env.CCDX_IMG_QUALITY, 82, 100);
|
|
16
17
|
const IMG_MIN_BYTES = nonNegativeInt(process.env.CCDX_IMG_MIN_BYTES, 100000);
|
|
17
18
|
const IMG_MAX_INPUT_PIXELS = positiveInt(process.env.CCDX_IMG_MAX_INPUT_PIXELS, DEFAULT_IMG_MAX_INPUT_PIXELS);
|
|
19
|
+
const IMG_CACHE_MAX_BYTES = nonNegativeInt(process.env.CCDX_IMG_CACHE_MAX_BYTES, DEFAULT_IMG_CACHE_MAX_BYTES);
|
|
18
20
|
const MAX_UPSTREAM_BODY_BYTES = positiveInt(process.env.CCDX_MAX_UPSTREAM_BODY_BYTES, DEFAULT_MAX_UPSTREAM_BODY_BYTES);
|
|
19
21
|
const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
|
|
20
22
|
const IMG_CONCURRENCY = parseImageConcurrency(process.env.CCDX_IMG_CONCURRENCY);
|
|
@@ -22,7 +24,8 @@ const OVERSIZE_IMAGE_PROFILES = [
|
|
|
22
24
|
{ maxDim: Math.min(IMG_MAX_DIM, 1600), quality: Math.min(IMG_QUALITY, 75) },
|
|
23
25
|
{ maxDim: Math.min(IMG_MAX_DIM, 1280), quality: Math.min(IMG_QUALITY, 65) },
|
|
24
26
|
];
|
|
25
|
-
const
|
|
27
|
+
const IMAGE_ENCODER_CONFIG = "webp:v1:effort=4:autorotate=1:fit=inside:without_enlargement=1";
|
|
28
|
+
const imageResultCache = createBoundedResultCache({ maxBytes: IMG_CACHE_MAX_BYTES });
|
|
26
29
|
let sharpImport = null;
|
|
27
30
|
|
|
28
31
|
function positiveInt(value, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
@@ -140,52 +143,50 @@ export function createTaskLimiter(concurrency) {
|
|
|
140
143
|
const runGlobalImageTask = createTaskLimiter(IMG_CONCURRENCY);
|
|
141
144
|
|
|
142
145
|
export function imageOptimizationStats() {
|
|
146
|
+
const cache = imageResultCache.stats();
|
|
143
147
|
return {
|
|
144
148
|
...runGlobalImageTask.stats(),
|
|
145
149
|
disabled: IMG_OPT_DISABLED,
|
|
146
|
-
cache_entries:
|
|
150
|
+
cache_entries: cache.entries,
|
|
151
|
+
cache_bytes: cache.bytes,
|
|
152
|
+
cache_max_bytes: cache.max_bytes,
|
|
153
|
+
cache_hits: cache.hits,
|
|
154
|
+
cache_misses: cache.misses,
|
|
155
|
+
cache_evictions: cache.evictions,
|
|
156
|
+
cache_inflight: cache.inflight,
|
|
147
157
|
sharp_loaded: sharpImport !== null,
|
|
148
158
|
};
|
|
149
159
|
}
|
|
150
160
|
|
|
161
|
+
export function resetImageOptimizationCacheForTests() {
|
|
162
|
+
imageResultCache.clear();
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
async function sharp() {
|
|
152
166
|
sharpImport ||= import("sharp");
|
|
153
167
|
const mod = await sharpImport;
|
|
154
168
|
return mod.default || mod;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
function imageDigest(
|
|
158
|
-
return createHash("sha256").update(
|
|
171
|
+
function imageDigest(value, encoding) {
|
|
172
|
+
return createHash("sha256").update(value, encoding).digest("base64url");
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
function imageOptimizationKey(digest, constraints, quality) {
|
|
175
|
+
function imageOptimizationKey(digest, mime, constraints, quality) {
|
|
162
176
|
return [
|
|
163
177
|
digest,
|
|
178
|
+
mime,
|
|
164
179
|
constraints.maxLongEdge || "",
|
|
165
180
|
constraints.maxShortEdge || "",
|
|
166
181
|
constraints.maxArea || "",
|
|
167
182
|
quality,
|
|
183
|
+
IMAGE_ENCODER_CONFIG,
|
|
168
184
|
].join(":");
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
function touchOptimizedImage(digest) {
|
|
172
|
-
if (!optimizedImageDigests.delete(digest)) return false;
|
|
173
|
-
optimizedImageDigests.add(digest);
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function rememberOptimizedImage(digest) {
|
|
178
|
-
optimizedImageDigests.delete(digest);
|
|
179
|
-
optimizedImageDigests.add(digest);
|
|
180
|
-
if (optimizedImageDigests.size > MAX_OPTIMIZED_IMAGE_DIGESTS) {
|
|
181
|
-
optimizedImageDigests.delete(optimizedImageDigests.values().next().value);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
187
|
export async function optimizeImageDataUrl(dataUrl, {
|
|
186
188
|
maxDim = IMG_MAX_DIM,
|
|
187
189
|
quality = IMG_QUALITY,
|
|
188
|
-
force = false,
|
|
189
190
|
model,
|
|
190
191
|
signal,
|
|
191
192
|
} = {}) {
|
|
@@ -197,52 +198,60 @@ export async function optimizeImageDataUrl(dataUrl, {
|
|
|
197
198
|
const encoded = match[2];
|
|
198
199
|
const inputBytes = Buffer.byteLength(encoded, "base64");
|
|
199
200
|
if (mime.includes("gif")) return dataUrl;
|
|
201
|
+
if (signal?.aborted) throw abortError(signal);
|
|
202
|
+
const constraints = imageConstraintsForModel(model, { maxDim });
|
|
203
|
+
const outputQuality = positiveInt(quality, IMG_QUALITY, 100);
|
|
204
|
+
// Hash decoded source bytes without retaining a decoded Buffer. Cache hits
|
|
205
|
+
// therefore bypass both Sharp and the global image work queue.
|
|
206
|
+
const digest = imageDigest(encoded, "base64");
|
|
207
|
+
const optimizationKey = imageOptimizationKey(digest, mime, constraints, outputQuality);
|
|
200
208
|
|
|
201
209
|
try {
|
|
202
|
-
return await
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
210
|
+
return await imageResultCache.getOrCreate(optimizationKey, (sharedSignal) => (
|
|
211
|
+
runGlobalImageTask(async () => {
|
|
212
|
+
const raw = Buffer.from(encoded, "base64");
|
|
213
|
+
const resize = await sharp();
|
|
214
|
+
const image = resize(raw, { failOn: "none", limitInputPixels: IMG_MAX_INPUT_PIXELS });
|
|
215
|
+
const metadata = await image.metadata();
|
|
216
|
+
if (sharedSignal.aborted) throw abortError(sharedSignal);
|
|
217
|
+
const orientation = Number(metadata.orientation);
|
|
218
|
+
const rotated = orientation >= 5 && orientation <= 8;
|
|
219
|
+
const sourceWidth = rotated ? metadata.height : metadata.width;
|
|
220
|
+
const sourceHeight = rotated ? metadata.width : metadata.height;
|
|
221
|
+
const target = fitImageDimensions(sourceWidth, sourceHeight, constraints);
|
|
222
|
+
const resizeRequired = target
|
|
223
|
+
&& (target.width < sourceWidth || target.height < sourceHeight);
|
|
224
|
+
if (raw.length < IMG_MIN_BYTES && !resizeRequired) return dataUrl;
|
|
225
|
+
const resizeOptions = target
|
|
226
|
+
? { ...target, fit: "inside", withoutEnlargement: true }
|
|
227
|
+
: {
|
|
228
|
+
width: constraints.maxLongEdge,
|
|
229
|
+
height: constraints.maxLongEdge,
|
|
230
|
+
fit: "inside",
|
|
231
|
+
withoutEnlargement: true,
|
|
232
|
+
};
|
|
233
|
+
const out = await image
|
|
234
|
+
.rotate()
|
|
235
|
+
.resize(resizeOptions)
|
|
236
|
+
.webp({ quality: outputQuality, effort: 4 })
|
|
237
|
+
.toBuffer();
|
|
238
|
+
if (sharedSignal.aborted) throw abortError(sharedSignal);
|
|
239
|
+
if (out.length >= raw.length) return dataUrl;
|
|
240
|
+
const outputBase64 = out.toString("base64");
|
|
241
|
+
const outputDataUrl = `data:image/webp;base64,${outputBase64}`;
|
|
242
|
+
const outputKey = imageOptimizationKey(
|
|
243
|
+
imageDigest(outputBase64, "base64"),
|
|
244
|
+
"image/webp",
|
|
245
|
+
constraints,
|
|
246
|
+
outputQuality,
|
|
247
|
+
);
|
|
248
|
+
// Preserve idempotence when optimized history is submitted again.
|
|
249
|
+
imageResultCache.set(outputKey, outputDataUrl);
|
|
250
|
+
const ratio = ((out.length / raw.length) * 100).toFixed(1);
|
|
251
|
+
console.log(status("info", `image ${(raw.length / 1024).toFixed(0)}KB ${mime} -> ${(out.length / 1024).toFixed(0)}KB webp (${ratio}%)`));
|
|
252
|
+
return outputDataUrl;
|
|
253
|
+
}, { signal: sharedSignal })
|
|
254
|
+
), { signal });
|
|
246
255
|
} catch (e) {
|
|
247
256
|
if (signal?.aborted || e?.name === "AbortError") throw e;
|
|
248
257
|
if (inputBytes >= IMG_MIN_BYTES) {
|
package/src/observability.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
|
-
import { adapterHealthPayload } from "./running-adapter.mjs";
|
|
2
|
+
import { ADAPTER_STATUS_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
3
3
|
import { copilotRuntimeStatus } from "./copilot.mjs";
|
|
4
4
|
import { imageOptimizationStats } from "./image-optimization.mjs";
|
|
5
5
|
import { responseHistoryStats } from "./response-history.mjs";
|
|
6
6
|
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export { ADAPTER_STATUS_PATH };
|
|
9
9
|
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
10
10
|
|
|
11
11
|
const ROUTE_NAMES = Object.freeze([
|
|
@@ -118,7 +118,7 @@ function modelRegistryStatus(modelRegistry) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export function runtimeStatusPayload({ metrics, admission, modelRegistry } = {}) {
|
|
121
|
+
export function runtimeStatusPayload({ metrics, streamPerformance, admission, modelRegistry } = {}) {
|
|
122
122
|
const memory = process.memoryUsage();
|
|
123
123
|
return {
|
|
124
124
|
...adapterHealthPayload(),
|
|
@@ -131,6 +131,7 @@ export function runtimeStatusPayload({ metrics, admission, modelRegistry } = {})
|
|
|
131
131
|
array_buffers_bytes: memory.arrayBuffers,
|
|
132
132
|
},
|
|
133
133
|
requests: metrics?.snapshot?.() || createRequestMetrics().snapshot(),
|
|
134
|
+
stream_performance: streamPerformance?.snapshot?.() || null,
|
|
134
135
|
admission: admission?.diagnostics?.() || admission?.stats?.() || null,
|
|
135
136
|
response_history: responseHistoryStats(),
|
|
136
137
|
image_optimization: imageOptimizationStats(),
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -2,6 +2,15 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chatCompletions } from "./copilot.mjs";
|
|
3
3
|
import { webStreamLines } from "./stream.mjs";
|
|
4
4
|
import { abortErrorStatusCode, isAbortLikeError } from "./http-transport.mjs";
|
|
5
|
+
import {
|
|
6
|
+
incompleteUpstreamStream,
|
|
7
|
+
invalidUpstreamStream,
|
|
8
|
+
requireUpstreamEventStream,
|
|
9
|
+
ToolArgumentDeltaGuard,
|
|
10
|
+
upstreamChatStreamError,
|
|
11
|
+
withChatStreamUsage,
|
|
12
|
+
} from "./stream-contract.mjs";
|
|
13
|
+
import { isChatOutputDelta, markFirstOutput, markOutputTokens } from "./stream-performance.mjs";
|
|
5
14
|
|
|
6
15
|
function cloneJson(value) {
|
|
7
16
|
return value === undefined ? undefined : structuredClone(value);
|
|
@@ -134,7 +143,7 @@ export function chatToResponses(chatResp, model) {
|
|
|
134
143
|
|
|
135
144
|
export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
136
145
|
delete chatReq.max_tokens;
|
|
137
|
-
const upstreamReq = chatReq
|
|
146
|
+
const upstreamReq = withChatStreamUsage(chatReq);
|
|
138
147
|
const bodyText = upstreamReq === chatReq ? options.bodyText : undefined;
|
|
139
148
|
let resp;
|
|
140
149
|
try {
|
|
@@ -150,9 +159,16 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
150
159
|
return;
|
|
151
160
|
}
|
|
152
161
|
if (!resp.ok) {
|
|
153
|
-
await onError(resp.status, await resp.text());
|
|
162
|
+
await onError(resp.status, await resp.text(), undefined, resp);
|
|
154
163
|
return;
|
|
155
164
|
}
|
|
165
|
+
try {
|
|
166
|
+
await requireUpstreamEventStream(resp);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
await onError(error.statusCode, error.message, error);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
options.onUpstreamResponse?.(resp);
|
|
156
172
|
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
157
173
|
const respId = `resp_${uid()}`;
|
|
158
174
|
let actualModel = chatReq.model || "unknown";
|
|
@@ -161,6 +177,13 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
161
177
|
let nextOutputIndex = 0;
|
|
162
178
|
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
163
179
|
const toolCalls = new Map();
|
|
180
|
+
const toolArgumentGuard = new ToolArgumentDeltaGuard();
|
|
181
|
+
let performanceOutputSeen = false;
|
|
182
|
+
const markPerformanceOutput = () => {
|
|
183
|
+
if (performanceOutputSeen) return;
|
|
184
|
+
performanceOutputSeen = true;
|
|
185
|
+
markFirstOutput();
|
|
186
|
+
};
|
|
164
187
|
|
|
165
188
|
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
166
189
|
|
|
@@ -258,7 +281,16 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
258
281
|
const data = line.slice(6).trim();
|
|
259
282
|
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
260
283
|
let parsed;
|
|
261
|
-
try {
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(data);
|
|
286
|
+
} catch {
|
|
287
|
+
throw invalidUpstreamStream(
|
|
288
|
+
"Upstream Chat Completions SSE event contained invalid JSON",
|
|
289
|
+
"upstream_stream_json_invalid",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const upstreamError = upstreamChatStreamError(parsed);
|
|
293
|
+
if (upstreamError) throw upstreamError;
|
|
262
294
|
if (parsed.model) actualModel = parsed.model;
|
|
263
295
|
if (parsed.usage) {
|
|
264
296
|
usage = {
|
|
@@ -268,9 +300,11 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
268
300
|
};
|
|
269
301
|
const cached = parsed.usage.prompt_tokens_details?.cached_tokens;
|
|
270
302
|
if (cached) usage.input_tokens_details = { cached_tokens: cached };
|
|
303
|
+
markOutputTokens(usage.output_tokens);
|
|
271
304
|
}
|
|
272
305
|
const delta = parsed.choices?.[0]?.delta;
|
|
273
306
|
if (!delta) continue;
|
|
307
|
+
if (isChatOutputDelta(delta)) markPerformanceOutput();
|
|
274
308
|
if (delta.content) {
|
|
275
309
|
const message = await ensureMessageItem();
|
|
276
310
|
fullText += delta.content;
|
|
@@ -281,22 +315,24 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
281
315
|
const { tool, created } = await ensureToolCall(tc);
|
|
282
316
|
if (tc.id) tool.callId = tc.id;
|
|
283
317
|
if (!created && tc.function?.name) tool.name += tc.function.name;
|
|
284
|
-
|
|
285
|
-
|
|
318
|
+
const argumentDelta = tc.function?.arguments;
|
|
319
|
+
toolArgumentGuard.observe(tool.outputIndex, argumentDelta);
|
|
320
|
+
if (argumentDelta) {
|
|
321
|
+
tool.arguments += argumentDelta;
|
|
286
322
|
await emitEvent("response.function_call_arguments.delta", {
|
|
287
323
|
output_index: tool.outputIndex,
|
|
288
324
|
item_id: tool.id,
|
|
289
|
-
delta:
|
|
325
|
+
delta: argumentDelta,
|
|
290
326
|
});
|
|
291
327
|
}
|
|
292
328
|
}
|
|
293
329
|
}
|
|
294
330
|
}
|
|
295
331
|
} catch (e) {
|
|
296
|
-
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
297
|
-
await onError(statusCode, e?.message || "upstream stream error");
|
|
332
|
+
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : (e?.statusCode || 502);
|
|
333
|
+
await onError(statusCode, e?.message || "upstream stream error", e);
|
|
298
334
|
return;
|
|
299
335
|
}
|
|
300
|
-
|
|
301
|
-
|
|
336
|
+
const error = incompleteUpstreamStream("[DONE]");
|
|
337
|
+
await onError(error.statusCode, error.message, error);
|
|
302
338
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./responses-byte-budget.mjs";
|
|
6
6
|
import { responsesToChat } from "./responses-bridge.mjs";
|
|
7
7
|
import { readResponsesImagePart, readResponsesToolOutputParts } from "./responses-content.mjs";
|
|
8
|
+
import { withChatStreamUsage } from "./stream-contract.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
|
|
10
11
|
|
|
@@ -30,7 +31,7 @@ function chatRequest(responsesBody, stream) {
|
|
|
30
31
|
const chatReq = responsesToChat(responsesBody);
|
|
31
32
|
chatReq.stream = Boolean(stream);
|
|
32
33
|
delete chatReq.max_tokens;
|
|
33
|
-
return chatReq;
|
|
34
|
+
return stream ? withChatStreamUsage(chatReq) : chatReq;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function adjustedResponsesTarget(limit, responsesBytes, chatBytes) {
|