codex-copilot-dx 0.6.1 → 0.6.3
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 +42 -21
- package/bin/cli.mjs +12 -4
- package/bin/codex-copilot-dx.mjs +2 -1
- package/package.json +1 -1
- package/scripts/benchmark.mjs +95 -0
- package/src/adapter.mjs +5 -0
- package/src/auth.mjs +5 -0
- package/src/cli-auth.mjs +43 -12
- package/src/cli-options.mjs +141 -59
- package/src/cli-status.mjs +40 -3
- package/src/copilot-client.mjs +5 -0
- package/src/doctor.mjs +56 -3
- package/src/http-transport.mjs +1 -1
- package/src/observability.mjs +4 -4
- package/src/pm-studio-relay.mjs +14 -48
- package/src/pm-studio-status.mjs +130 -0
- package/src/profile-model-runtime.mjs +3 -2
- package/src/profile-routing.mjs +74 -0
- package/src/profile-runtime.mjs +13 -0
- package/src/response-history.mjs +4 -0
- package/src/responses-bridge.mjs +12 -6
- package/src/responses-byte-budget.mjs +63 -5
- package/src/responses-handler.mjs +60 -16
- package/src/responses-image-pressure.mjs +163 -0
- package/src/responses-proxy.mjs +7 -4
- package/src/responses-request.mjs +9 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
AUTH_PROFILE_CLAUDE,
|
|
6
|
+
readAuthProfileCredentials,
|
|
7
|
+
} from "./auth-profile.mjs";
|
|
8
|
+
import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createPmStudioSetupOperations,
|
|
11
|
+
DEFAULT_PM_STUDIO_APP_PATH,
|
|
12
|
+
inspectPmStudioApp,
|
|
13
|
+
PM_STUDIO_CLAUDE_AUTH_COMMAND,
|
|
14
|
+
} from "./pm-studio-setup.mjs";
|
|
15
|
+
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
16
|
+
import { status } from "./status.mjs";
|
|
17
|
+
|
|
18
|
+
const PM_RELAY_HOST = "127.0.0.1";
|
|
19
|
+
const PM_RELAY_PORT = 2026;
|
|
20
|
+
|
|
21
|
+
function selectRecipe(metadata, recipes) {
|
|
22
|
+
return recipes.find((recipe) => recipe.version === String(metadata.version)
|
|
23
|
+
&& recipe.build === String(metadata.build)
|
|
24
|
+
&& recipe.bundleIdentifier === String(metadata.bundleIdentifier));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeMessage(error) {
|
|
28
|
+
return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inspectClaudeProfile(home, readCredentials) {
|
|
32
|
+
try {
|
|
33
|
+
const credentials = readCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
34
|
+
return {
|
|
35
|
+
configured: credentials.configured === true,
|
|
36
|
+
valid: credentials.valid === true,
|
|
37
|
+
reason: credentials.reason || "",
|
|
38
|
+
login: credentials.identity?.login || "",
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { configured: true, valid: false, reason: safeMessage(error), login: "" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function inspectPmStudioStatus({
|
|
46
|
+
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
47
|
+
home = os.homedir(),
|
|
48
|
+
recipes = PM_STUDIO_RECIPES,
|
|
49
|
+
operationOverrides = {},
|
|
50
|
+
existsSync = fs.existsSync,
|
|
51
|
+
inspectApp = inspectPmStudioApp,
|
|
52
|
+
readClaudeCredentials = readAuthProfileCredentials,
|
|
53
|
+
checkRunningAdapterFn = checkRunningAdapter,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
56
|
+
let app;
|
|
57
|
+
if (!existsSync(appPath)) {
|
|
58
|
+
app = { state: "not_installed", metadata: null, issues: [] };
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
const metadata = operations.readBundleMetadata({
|
|
62
|
+
appPath,
|
|
63
|
+
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
64
|
+
processRunner: operations.processRunner,
|
|
65
|
+
});
|
|
66
|
+
const normalized = {
|
|
67
|
+
version: String(metadata.version),
|
|
68
|
+
build: String(metadata.build),
|
|
69
|
+
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
70
|
+
};
|
|
71
|
+
const recipe = selectRecipe(normalized, recipes);
|
|
72
|
+
app = recipe
|
|
73
|
+
? { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id }
|
|
74
|
+
: { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const claude = inspectClaudeProfile(home, readClaudeCredentials);
|
|
81
|
+
let adapter;
|
|
82
|
+
try {
|
|
83
|
+
adapter = await checkRunningAdapterFn({ host: PM_RELAY_HOST, port: PM_RELAY_PORT });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
adapter = { ok: false, error };
|
|
86
|
+
}
|
|
87
|
+
const operational = app.state === "patched" && claude.valid && adapter?.ok === true;
|
|
88
|
+
return { ok: operational, appPath, app, claude, adapter };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function appStatusLine(result, commandName) {
|
|
92
|
+
const { app } = result;
|
|
93
|
+
const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
|
|
94
|
+
if (app.state === "patched") return status("ok", `PM Studio ${version} is patched and verified`);
|
|
95
|
+
if (app.state === "clean") return status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
|
|
96
|
+
if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
|
|
97
|
+
if (app.state === "drift") return status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
|
|
98
|
+
if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
|
|
99
|
+
return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
|
|
103
|
+
const lines = [
|
|
104
|
+
`${commandName} pms status`,
|
|
105
|
+
appStatusLine(result, commandName),
|
|
106
|
+
];
|
|
107
|
+
if (result.claude.valid) {
|
|
108
|
+
const account = result.claude.login ? ` for ${result.claude.login}` : "";
|
|
109
|
+
lines.push(status("ok", `Isolated Claude profile is valid${account}`));
|
|
110
|
+
} else {
|
|
111
|
+
const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
|
|
112
|
+
lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
|
|
113
|
+
}
|
|
114
|
+
if (result.adapter?.ok) {
|
|
115
|
+
lines.push(status("ok", `PM relay is available at ${result.adapter.baseUrl}`));
|
|
116
|
+
} else if (result.adapter?.incompatible) {
|
|
117
|
+
lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
|
|
118
|
+
} else {
|
|
119
|
+
lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
|
|
120
|
+
}
|
|
121
|
+
lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runPmStudioStatus(options = {}) {
|
|
126
|
+
const result = await inspectPmStudioStatus(options);
|
|
127
|
+
const output = formatPmStudioStatus(result, options);
|
|
128
|
+
(options.log || console.log)(output);
|
|
129
|
+
return { ...result, output };
|
|
130
|
+
}
|
|
@@ -33,6 +33,7 @@ export function createProfileModelRuntime({
|
|
|
33
33
|
codexClient,
|
|
34
34
|
claudeClient = codexClient,
|
|
35
35
|
claudeMode = "inherited",
|
|
36
|
+
codexCredentialFingerprint = "",
|
|
36
37
|
claudeCredentialFingerprint = "",
|
|
37
38
|
home,
|
|
38
39
|
env = process.env,
|
|
@@ -93,7 +94,7 @@ export function createProfileModelRuntime({
|
|
|
93
94
|
function loadCached({ client, profile, registry, ownsClaudeModels }) {
|
|
94
95
|
const credentialFingerprint = profile === "claude"
|
|
95
96
|
? String(claudeCredentialFingerprint || "").trim()
|
|
96
|
-
: "";
|
|
97
|
+
: String(codexCredentialFingerprint || "").trim();
|
|
97
98
|
if (profile === "claude" && !credentialFingerprint) return false;
|
|
98
99
|
const cached = loadModelCache({ home, profile, credentialFingerprint });
|
|
99
100
|
if (!cached) return false;
|
|
@@ -133,7 +134,7 @@ export function createProfileModelRuntime({
|
|
|
133
134
|
function persist(models, profile) {
|
|
134
135
|
const credentialFingerprint = profile === "claude"
|
|
135
136
|
? String(claudeCredentialFingerprint || "").trim()
|
|
136
|
-
: "";
|
|
137
|
+
: String(codexCredentialFingerprint || "").trim();
|
|
137
138
|
if (profile === "claude" && !credentialFingerprint) return;
|
|
138
139
|
try {
|
|
139
140
|
saveModelCache(models, { home, profile, credentialFingerprint });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function catalogData(catalog) {
|
|
2
|
+
if (Array.isArray(catalog)) return catalog;
|
|
3
|
+
return Array.isArray(catalog?.data) ? catalog.data : [];
|
|
4
|
+
}
|
|
5
|
+
|
|
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
|
+
export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
|
|
21
|
+
const isolated = claudeMode === "isolated" || claudeConfigured === true;
|
|
22
|
+
return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {}) {
|
|
26
|
+
if (typeof getCatalog !== "function") throw new Error("getCatalog is required");
|
|
27
|
+
if (typeof isClaudeEnabled !== "function") throw new Error("isClaudeEnabled is required");
|
|
28
|
+
|
|
29
|
+
const unset = Symbol("unset");
|
|
30
|
+
let cachedCatalog = unset;
|
|
31
|
+
let cachedEnabled = false;
|
|
32
|
+
let cachedAllowed = new Map();
|
|
33
|
+
let cachedKnown = new Set();
|
|
34
|
+
let rebuilds = 0;
|
|
35
|
+
|
|
36
|
+
function availability() {
|
|
37
|
+
const catalog = getCatalog();
|
|
38
|
+
const enabled = isClaudeEnabled() === true;
|
|
39
|
+
if (catalog === cachedCatalog && enabled === cachedEnabled) {
|
|
40
|
+
return { allowed: cachedAllowed, known: cachedKnown };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allowed = new Map();
|
|
44
|
+
const known = new Set();
|
|
45
|
+
for (const model of catalogData(catalog)) {
|
|
46
|
+
const id = String(model?.id || "").trim();
|
|
47
|
+
if (!id || !isClaudeCatalogEntry(model)) continue;
|
|
48
|
+
known.add(id);
|
|
49
|
+
if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
50
|
+
}
|
|
51
|
+
cachedCatalog = catalog;
|
|
52
|
+
cachedEnabled = enabled;
|
|
53
|
+
cachedAllowed = allowed;
|
|
54
|
+
cachedKnown = known;
|
|
55
|
+
rebuilds += 1;
|
|
56
|
+
return { allowed, known };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
classify(modelId) {
|
|
61
|
+
const id = String(modelId || "").trim();
|
|
62
|
+
const { allowed, known } = availability();
|
|
63
|
+
if (allowed.has(id)) return "claude";
|
|
64
|
+
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
65
|
+
return "enterprise";
|
|
66
|
+
},
|
|
67
|
+
allowedModels() {
|
|
68
|
+
return [...availability().allowed.values()];
|
|
69
|
+
},
|
|
70
|
+
diagnostics() {
|
|
71
|
+
return { rebuilds, allowed: cachedAllowed.size, known: cachedKnown.size };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/profile-runtime.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AUTH_PROFILE_CLAUDE,
|
|
3
|
+
AUTH_PROFILE_CODEX,
|
|
3
4
|
authProfilePaths,
|
|
4
5
|
profileReauthMessage,
|
|
5
6
|
readAuthProfileCredentials,
|
|
@@ -45,6 +46,16 @@ export function createProfileRuntime({
|
|
|
45
46
|
};
|
|
46
47
|
const credentials = readCredentials();
|
|
47
48
|
const claudeProfile = safeProfileStatus(credentials);
|
|
49
|
+
let codexCredentialFingerprint = "";
|
|
50
|
+
try {
|
|
51
|
+
const codexCredentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
52
|
+
if (codexCredentials.valid) {
|
|
53
|
+
codexCredentialFingerprint = githubTokenFingerprint(codexCredentials.token);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// The Codex client remains authoritative if its optional metadata cannot
|
|
57
|
+
// be inspected; only credential-bound cache reuse is disabled.
|
|
58
|
+
}
|
|
48
59
|
|
|
49
60
|
if (!credentials.configured) {
|
|
50
61
|
return Object.freeze({
|
|
@@ -52,6 +63,7 @@ export function createProfileRuntime({
|
|
|
52
63
|
claudeClient: codexClient,
|
|
53
64
|
claudeMode: "inherited",
|
|
54
65
|
claudeProfile,
|
|
66
|
+
codexCredentialFingerprint,
|
|
55
67
|
claudeCredentialFingerprint: "",
|
|
56
68
|
});
|
|
57
69
|
}
|
|
@@ -73,6 +85,7 @@ export function createProfileRuntime({
|
|
|
73
85
|
claudeClient,
|
|
74
86
|
claudeMode: "isolated",
|
|
75
87
|
claudeProfile,
|
|
88
|
+
codexCredentialFingerprint,
|
|
76
89
|
claudeCredentialFingerprint: credentials.valid
|
|
77
90
|
? githubTokenFingerprint(credentials.token)
|
|
78
91
|
: "",
|
package/src/response-history.mjs
CHANGED
|
@@ -174,6 +174,10 @@ export function responseHistoryStats() {
|
|
|
174
174
|
return { entries: histories.size, bytes: totalBytes, evicted: evictedIds.size };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
export function responseHistoryRootId(responseId) {
|
|
178
|
+
return histories.get(responseId)?.rootId || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
177
181
|
export function materializeResponseHistory(responseId) {
|
|
178
182
|
const chain = [];
|
|
179
183
|
const seen = new Set();
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -149,24 +149,29 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
149
149
|
try {
|
|
150
150
|
const chatCompletionsFn = options.chatCompletionsFn || chatCompletions;
|
|
151
151
|
try {
|
|
152
|
-
|
|
152
|
+
options.onUpstreamStart?.();
|
|
153
|
+
resp = await chatCompletionsFn(upstreamReq, {
|
|
154
|
+
signal: options.signal,
|
|
155
|
+
bodyText,
|
|
156
|
+
onUpstreamStart: options.onUpstreamStart,
|
|
157
|
+
});
|
|
153
158
|
} finally {
|
|
154
159
|
options.releaseRequest?.();
|
|
155
160
|
}
|
|
156
161
|
} catch (e) {
|
|
157
162
|
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
158
163
|
await onError(statusCode, e.message);
|
|
159
|
-
return;
|
|
164
|
+
return false;
|
|
160
165
|
}
|
|
161
166
|
if (!resp.ok) {
|
|
162
167
|
await onError(resp.status, await resp.text(), undefined, resp);
|
|
163
|
-
return;
|
|
168
|
+
return false;
|
|
164
169
|
}
|
|
165
170
|
try {
|
|
166
171
|
await requireUpstreamEventStream(resp);
|
|
167
172
|
} catch (error) {
|
|
168
173
|
await onError(error.statusCode, error.message, error);
|
|
169
|
-
return;
|
|
174
|
+
return false;
|
|
170
175
|
}
|
|
171
176
|
options.onUpstreamResponse?.(resp);
|
|
172
177
|
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
@@ -279,7 +284,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
279
284
|
})) {
|
|
280
285
|
if (!line.startsWith("data: ")) continue;
|
|
281
286
|
const data = line.slice(6).trim();
|
|
282
|
-
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
287
|
+
if (data === "[DONE]") { await emitCompleted(); onDone(); return true; }
|
|
283
288
|
let parsed;
|
|
284
289
|
try {
|
|
285
290
|
parsed = JSON.parse(data);
|
|
@@ -331,8 +336,9 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
331
336
|
} catch (e) {
|
|
332
337
|
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : (e?.statusCode || 502);
|
|
333
338
|
await onError(statusCode, e?.message || "upstream stream error", e);
|
|
334
|
-
return;
|
|
339
|
+
return false;
|
|
335
340
|
}
|
|
336
341
|
const error = incompleteUpstreamStream("[DONE]");
|
|
337
342
|
await onError(error.statusCode, error.message, error);
|
|
343
|
+
return false;
|
|
338
344
|
}
|
|
@@ -159,12 +159,14 @@ function recordBytesAfterDrop(record, state, partIndex) {
|
|
|
159
159
|
return state.baseBytes + remainingElementBytes + remainingCount - 1;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function selectImageDrops(refs, bodyBytes, targetBytes) {
|
|
162
|
+
function selectImageDrops(refs, bodyBytes, targetBytes, maxHistoricalImages = Number.POSITIVE_INFINITY) {
|
|
163
163
|
const candidates = orderedHistoricalImageCandidates(refs);
|
|
164
164
|
const selected = new Set();
|
|
165
165
|
const byRecord = new Map();
|
|
166
166
|
let projectedBytes = bodyBytes;
|
|
167
|
+
let historicalImages = refs.reduce((count, ref) => count + (ref.historical ? 1 : 0), 0);
|
|
167
168
|
for (const index of candidates) {
|
|
169
|
+
if (projectedBytes <= targetBytes && historicalImages <= maxHistoricalImages) break;
|
|
168
170
|
const ref = refs[index];
|
|
169
171
|
let state = byRecord.get(ref.record);
|
|
170
172
|
if (!state) {
|
|
@@ -173,7 +175,7 @@ function selectImageDrops(refs, bodyBytes, targetBytes) {
|
|
|
173
175
|
}
|
|
174
176
|
const afterBytes = recordBytesAfterDrop(ref.record, state, ref.partIndex);
|
|
175
177
|
const savings = state.currentBytes - afterBytes;
|
|
176
|
-
if (savings <= 0) continue;
|
|
178
|
+
if (savings <= 0 && historicalImages <= maxHistoricalImages) continue;
|
|
177
179
|
state.currentBytes = afterBytes;
|
|
178
180
|
if (ref.record.kind !== "top-level") {
|
|
179
181
|
state.remainingCount -= 1;
|
|
@@ -181,9 +183,9 @@ function selectImageDrops(refs, bodyBytes, targetBytes) {
|
|
|
181
183
|
}
|
|
182
184
|
selected.add(index);
|
|
183
185
|
projectedBytes -= savings;
|
|
184
|
-
|
|
186
|
+
historicalImages -= 1;
|
|
185
187
|
}
|
|
186
|
-
return selected;
|
|
188
|
+
return { selected, historicalImages, projectedBytes };
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
function applyImageDrops(records, refs, selected) {
|
|
@@ -253,7 +255,7 @@ export function trimResponsesHistoryToByteBudget(reqBody, {
|
|
|
253
255
|
while (bodyBytes > limit) {
|
|
254
256
|
const { records, refs } = collectResponseImages(inputItems, currentInputStart);
|
|
255
257
|
if (!refs.length) break;
|
|
256
|
-
const selected = selectImageDrops(refs, bodyBytes, limit);
|
|
258
|
+
const { selected } = selectImageDrops(refs, bodyBytes, limit);
|
|
257
259
|
if (!selected.size) break;
|
|
258
260
|
applyImageDrops(records, refs, selected);
|
|
259
261
|
imagesOmitted += selected.size;
|
|
@@ -278,6 +280,62 @@ export function trimResponsesHistoryToByteBudget(reqBody, {
|
|
|
278
280
|
};
|
|
279
281
|
}
|
|
280
282
|
|
|
283
|
+
export function responsesHistoricalImageStats(inputItems, currentInputStart = 0) {
|
|
284
|
+
const items = Array.isArray(inputItems) ? inputItems : [];
|
|
285
|
+
const { refs } = collectResponseImages(items, currentInputStart);
|
|
286
|
+
const historicalImages = refs.reduce((count, ref) => count + (ref.historical ? 1 : 0), 0);
|
|
287
|
+
return {
|
|
288
|
+
totalImages: refs.length,
|
|
289
|
+
historicalImages,
|
|
290
|
+
currentImages: refs.length - historicalImages,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function trimResponsesHistoricalImages(reqBody, {
|
|
295
|
+
currentInputStart = 0,
|
|
296
|
+
maxHistoricalImages = Number.POSITIVE_INFINITY,
|
|
297
|
+
targetBytes = Number.POSITIVE_INFINITY,
|
|
298
|
+
initialBodyText,
|
|
299
|
+
initialBodyBytes,
|
|
300
|
+
} = {}) {
|
|
301
|
+
let bodyText = initialBodyText ?? JSON.stringify(reqBody);
|
|
302
|
+
let bodyBytes = initialBodyBytes ?? Buffer.byteLength(bodyText);
|
|
303
|
+
const byteLimit = Number.isFinite(targetBytes) && targetBytes > 0
|
|
304
|
+
? Math.floor(targetBytes)
|
|
305
|
+
: Number.POSITIVE_INFINITY;
|
|
306
|
+
const imageLimit = Number.isFinite(maxHistoricalImages) && maxHistoricalImages >= 0
|
|
307
|
+
? Math.floor(maxHistoricalImages)
|
|
308
|
+
: Number.POSITIVE_INFINITY;
|
|
309
|
+
const inputItems = Array.isArray(reqBody?.input) ? reqBody.input : [];
|
|
310
|
+
let imagesOmitted = 0;
|
|
311
|
+
|
|
312
|
+
while (true) {
|
|
313
|
+
const { records, refs } = collectResponseImages(inputItems, currentInputStart);
|
|
314
|
+
const historicalImages = refs.reduce((count, ref) => count + (ref.historical ? 1 : 0), 0);
|
|
315
|
+
if (bodyBytes <= byteLimit && historicalImages <= imageLimit) break;
|
|
316
|
+
if (!historicalImages) break;
|
|
317
|
+
const { selected } = selectImageDrops(refs, bodyBytes, byteLimit, imageLimit);
|
|
318
|
+
if (!selected.size) break;
|
|
319
|
+
applyImageDrops(records, refs, selected);
|
|
320
|
+
imagesOmitted += selected.size;
|
|
321
|
+
({ bodyText, bodyBytes } = bodyPayload(reqBody));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const stats = responsesHistoricalImageStats(inputItems, currentInputStart);
|
|
325
|
+
return {
|
|
326
|
+
bodyText,
|
|
327
|
+
bodyBytes,
|
|
328
|
+
targetBytes: byteLimit,
|
|
329
|
+
maxHistoricalImages: imageLimit,
|
|
330
|
+
historicalImages: stats.historicalImages,
|
|
331
|
+
currentImages: stats.currentImages,
|
|
332
|
+
imagesOmitted,
|
|
333
|
+
overBudget: bodyBytes > byteLimit,
|
|
334
|
+
overImageBudget: stats.historicalImages > imageLimit,
|
|
335
|
+
adapted: imagesOmitted > 0,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
281
339
|
export function enforceResponsesPayloadByteBudget(reqBody, payload) {
|
|
282
340
|
let finalized = payload;
|
|
283
341
|
if (payload?.overBudget) {
|
|
@@ -79,6 +79,7 @@ export function createResponsesHandler(options) {
|
|
|
79
79
|
autoReviewModelResolver,
|
|
80
80
|
chatCompletionsFn,
|
|
81
81
|
getCachedModelEndpointsFn = getCachedModelEndpoints,
|
|
82
|
+
imagePressure,
|
|
82
83
|
openAIModelEnv,
|
|
83
84
|
responsesPayloadOptions,
|
|
84
85
|
responsesFn,
|
|
@@ -90,14 +91,42 @@ export function createResponsesHandler(options) {
|
|
|
90
91
|
return async function handleResponses(req, res) {
|
|
91
92
|
const abort = createRequestAbort(req, res);
|
|
92
93
|
let releaseRequest = () => {};
|
|
94
|
+
let prepared = null;
|
|
95
|
+
let imagePressureResult = null;
|
|
96
|
+
let parsed = null;
|
|
97
|
+
const startUpstreamTimeout = () => abort.setTimeout(
|
|
98
|
+
parsed?.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
99
|
+
parsed?.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
100
|
+
);
|
|
101
|
+
const applyAdaptiveTimeout = (error) => {
|
|
102
|
+
if (!["responses_prepare_timeout", "stream_handshake_timeout", "upstream_timeout"].includes(abort.reason)) {
|
|
103
|
+
return error;
|
|
104
|
+
}
|
|
105
|
+
const activated = imagePressure?.markTimeout?.(prepared?.historyRootId, {
|
|
106
|
+
eligible: imagePressureResult?.pressureEligible === true,
|
|
107
|
+
});
|
|
108
|
+
if (!activated) return error;
|
|
109
|
+
const message = "Responses timed out while processing a large visual history. Retry the request; ccdx will keep fewer earlier images automatically. Restart is not required.";
|
|
110
|
+
error.statusCode = 504;
|
|
111
|
+
error.jsonBody = {
|
|
112
|
+
error: {
|
|
113
|
+
message,
|
|
114
|
+
type: "timeout_error",
|
|
115
|
+
code: "ccdx_visual_history_timeout",
|
|
116
|
+
retryable: true,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
return error;
|
|
120
|
+
};
|
|
93
121
|
try {
|
|
94
122
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
95
|
-
|
|
96
|
-
abort.setTimeout(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
123
|
+
parsed = await readJsonBody(req);
|
|
124
|
+
abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
|
|
125
|
+
prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
126
|
+
imagePressureResult = imagePressure?.apply?.(prepared) || null;
|
|
127
|
+
if (imagePressureResult?.adapted) {
|
|
128
|
+
console.warn(status("warn", `responses visual history mode=${imagePressureResult.mode} historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
|
|
129
|
+
}
|
|
101
130
|
prepared.surface = "responses";
|
|
102
131
|
const model = parsed.model || "unknown";
|
|
103
132
|
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
@@ -110,12 +139,14 @@ export function createResponsesHandler(options) {
|
|
|
110
139
|
|| (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
|
|
111
140
|
if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
|
|
112
141
|
if (useNativeResponses) {
|
|
113
|
-
await proxyCopilotResponses(prepared, req, res, responsesFn, {
|
|
142
|
+
const result = await proxyCopilotResponses(prepared, req, res, responsesFn, {
|
|
114
143
|
signal: abort.signal,
|
|
115
144
|
abort,
|
|
145
|
+
onUpstreamStart: startUpstreamTimeout,
|
|
116
146
|
releaseRequest,
|
|
117
147
|
streamIdleTimeoutMs,
|
|
118
148
|
});
|
|
149
|
+
if (result?.successful) imagePressure?.markSuccess?.(prepared.historyRootId);
|
|
119
150
|
} else {
|
|
120
151
|
const chatPayload = await prepareResponsesChatPayload(prepared, {
|
|
121
152
|
payloadOptions: responsesPayloadOptions,
|
|
@@ -125,7 +156,7 @@ export function createResponsesHandler(options) {
|
|
|
125
156
|
const { chatReq, bodyText } = chatPayload;
|
|
126
157
|
if (parsed.stream) {
|
|
127
158
|
let streamResponseHeaders = null;
|
|
128
|
-
await forwardToChat(chatReq, async (event, data) => {
|
|
159
|
+
const successful = await forwardToChat(chatReq, async (event, data) => {
|
|
129
160
|
if (!res.headersSent) res.writeHead(200, {
|
|
130
161
|
...streamResponseHeaders,
|
|
131
162
|
"Cache-Control": "no-cache",
|
|
@@ -142,7 +173,7 @@ export function createResponsesHandler(options) {
|
|
|
142
173
|
sendUpstreamError(res, upstreamResponse, errMsg);
|
|
143
174
|
return;
|
|
144
175
|
}
|
|
145
|
-
const responseError = error || Object.assign(new Error(errMsg), { statusCode });
|
|
176
|
+
const responseError = applyAdaptiveTimeout(error || Object.assign(new Error(errMsg), { statusCode }));
|
|
146
177
|
sendJsonError(res, responseError, statusCode || 500);
|
|
147
178
|
return;
|
|
148
179
|
}
|
|
@@ -150,6 +181,7 @@ export function createResponsesHandler(options) {
|
|
|
150
181
|
}, {
|
|
151
182
|
signal: abort.signal,
|
|
152
183
|
abort,
|
|
184
|
+
onUpstreamStart: startUpstreamTimeout,
|
|
153
185
|
streamIdleTimeoutMs,
|
|
154
186
|
chatCompletionsFn,
|
|
155
187
|
releaseRequest,
|
|
@@ -160,9 +192,15 @@ export function createResponsesHandler(options) {
|
|
|
160
192
|
});
|
|
161
193
|
},
|
|
162
194
|
});
|
|
195
|
+
if (successful) imagePressure?.markSuccess?.(prepared.historyRootId);
|
|
163
196
|
} else {
|
|
164
197
|
try {
|
|
165
|
-
|
|
198
|
+
startUpstreamTimeout();
|
|
199
|
+
const upstream = await chatCompletionsFn(chatReq, {
|
|
200
|
+
signal: abort.signal,
|
|
201
|
+
bodyText,
|
|
202
|
+
onUpstreamStart: startUpstreamTimeout,
|
|
203
|
+
});
|
|
166
204
|
releaseRequest();
|
|
167
205
|
const data = await upstream.text();
|
|
168
206
|
if (!upstream.ok) {
|
|
@@ -171,20 +209,23 @@ export function createResponsesHandler(options) {
|
|
|
171
209
|
}
|
|
172
210
|
const response = chatToResponses(JSON.parse(data), model);
|
|
173
211
|
rememberResponseHistory(prepared, response);
|
|
212
|
+
imagePressure?.markSuccess?.(prepared.historyRootId);
|
|
174
213
|
recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
|
|
175
214
|
res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
|
|
176
215
|
contentType: "application/json",
|
|
177
216
|
}));
|
|
178
217
|
res.end(JSON.stringify(response));
|
|
179
218
|
} catch (error) {
|
|
180
|
-
|
|
181
|
-
|
|
219
|
+
const responseError = applyAdaptiveTimeout(error);
|
|
220
|
+
logRequestFailure("Responses", responseError, abort);
|
|
221
|
+
sendJsonError(res, responseError, 502);
|
|
182
222
|
}
|
|
183
223
|
}
|
|
184
224
|
}
|
|
185
225
|
} catch (error) {
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
const responseError = applyAdaptiveTimeout(error);
|
|
227
|
+
logRequestFailure("Responses", responseError, abort);
|
|
228
|
+
sendJsonError(res, responseError, 502);
|
|
188
229
|
} finally {
|
|
189
230
|
releaseRequest();
|
|
190
231
|
abort.cleanup();
|
|
@@ -196,6 +237,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
196
237
|
const {
|
|
197
238
|
acquireRequest,
|
|
198
239
|
autoReviewModelResolver,
|
|
240
|
+
imagePressure,
|
|
199
241
|
openAIModelEnv,
|
|
200
242
|
responsesCompactFn,
|
|
201
243
|
upstreamTimeoutMs,
|
|
@@ -207,7 +249,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
207
249
|
try {
|
|
208
250
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
209
251
|
const parsed = await readJsonBody(req);
|
|
210
|
-
abort.setTimeout(upstreamTimeoutMs, "
|
|
252
|
+
abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
|
|
211
253
|
const prepared = prepareResponsesCompactionRequest(
|
|
212
254
|
prepareResponsesRequest(parsed, { mutate: true }),
|
|
213
255
|
);
|
|
@@ -217,11 +259,13 @@ export function createResponsesCompactHandler(options) {
|
|
|
217
259
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
218
260
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
219
261
|
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
|
|
220
|
-
await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
262
|
+
const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
221
263
|
signal: abort.signal,
|
|
222
264
|
abort,
|
|
265
|
+
onUpstreamStart: () => abort.setTimeout(upstreamTimeoutMs, "upstream_timeout"),
|
|
223
266
|
releaseRequest,
|
|
224
267
|
});
|
|
268
|
+
if (result?.compacted) imagePressure?.clear?.(prepared.historyRootId);
|
|
225
269
|
} catch (error) {
|
|
226
270
|
logRequestFailure("Responses compact", error, abort);
|
|
227
271
|
sendJsonError(res, error, 502);
|