codex-copilot-dx 0.4.40 → 0.4.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +2 -2
- package/src/responses-image-limit.mjs +220 -0
- package/src/responses-request.mjs +18 -3
package/README.md
CHANGED
|
@@ -179,6 +179,8 @@ Long computer-use sessions can accumulate screenshots inside the conversation hi
|
|
|
179
179
|
|
|
180
180
|
The adapter automatically downsamples embedded screenshots to long-edge <= 2048 px and re-encodes them as WebP before forwarding `/v1/responses`. It keeps an existing in-bounds WebP unchanged, never replaces an image with a larger encoding, and applies one global concurrency limit across direct and tool-output images.
|
|
181
181
|
|
|
182
|
+
When expanded Responses history exceeds Copilot's 50-image request limit, the adapter removes older duplicate image occurrences first, then keeps the 50 most recent images. This applies only to the forwarded request: local history remains complete, current images are preferred over older history, and requests at or below the limit are unchanged.
|
|
183
|
+
|
|
182
184
|
Newer ChatGPT/Codex clients can advertise an `image_gen` namespace that already exists upstream. The adapter removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image inputs and screenshot optimization remain enabled.
|
|
183
185
|
|
|
184
186
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.42",
|
|
4
4
|
"description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
|
|
5
5
|
"author": "Dale Xiao",
|
|
6
6
|
"type": "module",
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"gpt-tokenizer": "^3.4.0",
|
|
54
|
-
"sharp": "^0.
|
|
54
|
+
"sharp": "^0.35.3"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export const MAX_UPSTREAM_IMAGES = 50;
|
|
2
|
+
|
|
3
|
+
function isImagePart(part) {
|
|
4
|
+
return part && typeof part === "object"
|
|
5
|
+
&& ["input_image", "image", "image_url"].includes(part.type);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function imageIdentity(part) {
|
|
9
|
+
if (!isImagePart(part)) return null;
|
|
10
|
+
|
|
11
|
+
if (part.type === "image") {
|
|
12
|
+
const source = part.source;
|
|
13
|
+
if (typeof source?.data === "string") {
|
|
14
|
+
return { kind: `image:${source.type || "base64"}:${source.media_type || ""}`, value: source.data };
|
|
15
|
+
}
|
|
16
|
+
if (typeof source?.url === "string") {
|
|
17
|
+
return { kind: `image:url:${part.detail || ""}`, value: source.url };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = part.image_url ?? part.url;
|
|
22
|
+
const url = typeof raw === "string" ? raw : raw?.url;
|
|
23
|
+
if (typeof url === "string") {
|
|
24
|
+
const detail = part.detail ?? raw?.detail ?? "";
|
|
25
|
+
return { kind: `${part.type}:url:${detail}`, value: url };
|
|
26
|
+
}
|
|
27
|
+
if (typeof part.file_id === "string") {
|
|
28
|
+
return { kind: `${part.type}:file`, value: part.file_id };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function markerPart(record, maxImages) {
|
|
34
|
+
const text = `[Earlier image omitted to stay within the upstream ${maxImages}-image request limit.]`;
|
|
35
|
+
if (record.kind === "message") {
|
|
36
|
+
return {
|
|
37
|
+
type: record.role === "assistant" ? "output_text" : "input_text",
|
|
38
|
+
text,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { type: record.usesAnthropicParts ? "text" : "input_text", text };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function addPartContainer(records, refs, parts, options) {
|
|
45
|
+
if (!Array.isArray(parts)) return;
|
|
46
|
+
const record = {
|
|
47
|
+
parts,
|
|
48
|
+
kind: options.kind,
|
|
49
|
+
role: options.role,
|
|
50
|
+
commit: options.commit,
|
|
51
|
+
usesAnthropicParts: false,
|
|
52
|
+
};
|
|
53
|
+
let found = false;
|
|
54
|
+
for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
|
|
55
|
+
const part = parts[partIndex];
|
|
56
|
+
if (!isImagePart(part)) continue;
|
|
57
|
+
found = true;
|
|
58
|
+
if (part.type === "image") record.usesAnthropicParts = true;
|
|
59
|
+
refs.push({
|
|
60
|
+
record,
|
|
61
|
+
partIndex,
|
|
62
|
+
itemIndex: options.itemIndex,
|
|
63
|
+
identity: imageIdentity(part),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (found) records.push(record);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function collectImages(inputItems) {
|
|
70
|
+
const records = [];
|
|
71
|
+
const refs = [];
|
|
72
|
+
const topLevelRecord = {
|
|
73
|
+
parts: inputItems,
|
|
74
|
+
kind: "top-level",
|
|
75
|
+
role: "user",
|
|
76
|
+
commit: null,
|
|
77
|
+
usesAnthropicParts: false,
|
|
78
|
+
};
|
|
79
|
+
let hasTopLevelImages = false;
|
|
80
|
+
|
|
81
|
+
for (let itemIndex = 0; itemIndex < inputItems.length; itemIndex += 1) {
|
|
82
|
+
const item = inputItems[itemIndex];
|
|
83
|
+
if (!item || typeof item !== "object") continue;
|
|
84
|
+
|
|
85
|
+
if (isImagePart(item)) {
|
|
86
|
+
hasTopLevelImages = true;
|
|
87
|
+
refs.push({
|
|
88
|
+
record: topLevelRecord,
|
|
89
|
+
partIndex: itemIndex,
|
|
90
|
+
itemIndex,
|
|
91
|
+
identity: imageIdentity(item),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (item.type === "message") {
|
|
96
|
+
addPartContainer(records, refs, item.content, {
|
|
97
|
+
kind: "message",
|
|
98
|
+
role: item.role,
|
|
99
|
+
itemIndex,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (item.type !== "function_call_output") continue;
|
|
104
|
+
if (Array.isArray(item.output)) {
|
|
105
|
+
addPartContainer(records, refs, item.output, {
|
|
106
|
+
kind: "tool-output",
|
|
107
|
+
itemIndex,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (typeof item.output !== "string" || !item.output.trim().startsWith("[")) continue;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(item.output);
|
|
114
|
+
if (!Array.isArray(parsed)) continue;
|
|
115
|
+
addPartContainer(records, refs, parsed, {
|
|
116
|
+
kind: "tool-output",
|
|
117
|
+
itemIndex,
|
|
118
|
+
commit: () => { item.output = JSON.stringify(parsed); },
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
// Preserve non-JSON tool output unchanged.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasTopLevelImages) records.push(topLevelRecord);
|
|
126
|
+
return { records, refs };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function applyDrops(records, refs, dropped, maxImages) {
|
|
130
|
+
const droppedByRecord = new Map();
|
|
131
|
+
for (const refIndex of dropped) {
|
|
132
|
+
const ref = refs[refIndex];
|
|
133
|
+
let indexes = droppedByRecord.get(ref.record);
|
|
134
|
+
if (!indexes) {
|
|
135
|
+
indexes = new Set();
|
|
136
|
+
droppedByRecord.set(ref.record, indexes);
|
|
137
|
+
}
|
|
138
|
+
indexes.add(ref.partIndex);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const record of records) {
|
|
142
|
+
const indexes = droppedByRecord.get(record);
|
|
143
|
+
if (!indexes) continue;
|
|
144
|
+
const kept = record.parts.filter((_, index) => !indexes.has(index));
|
|
145
|
+
if (!kept.length && record.kind !== "top-level") kept.push(markerPart(record, maxImages));
|
|
146
|
+
record.parts.splice(0, record.parts.length, ...kept);
|
|
147
|
+
record.commit?.();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function enforceResponsesImageLimit(inputItems, {
|
|
152
|
+
currentInputStart = 0,
|
|
153
|
+
maxImages = MAX_UPSTREAM_IMAGES,
|
|
154
|
+
beforeMutate = () => {},
|
|
155
|
+
} = {}) {
|
|
156
|
+
const limit = Number.isFinite(maxImages) && maxImages > 0
|
|
157
|
+
? Math.floor(maxImages)
|
|
158
|
+
: MAX_UPSTREAM_IMAGES;
|
|
159
|
+
if (!Array.isArray(inputItems)) {
|
|
160
|
+
return { total: 0, kept: 0, omitted: 0, duplicates: 0, historicalOmitted: 0, currentOmitted: 0 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { records, refs } = collectImages(inputItems);
|
|
164
|
+
if (refs.length <= limit) {
|
|
165
|
+
return { total: refs.length, kept: refs.length, omitted: 0, duplicates: 0, historicalOmitted: 0, currentOmitted: 0 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const dropped = new Set();
|
|
169
|
+
const seenByKind = new Map();
|
|
170
|
+
const duplicateCandidates = [];
|
|
171
|
+
let kept = refs.length;
|
|
172
|
+
let duplicates = 0;
|
|
173
|
+
|
|
174
|
+
for (let index = refs.length - 1; index >= 0; index -= 1) {
|
|
175
|
+
const identity = refs[index].identity;
|
|
176
|
+
if (!identity) continue;
|
|
177
|
+
let seen = seenByKind.get(identity.kind);
|
|
178
|
+
if (!seen) {
|
|
179
|
+
seen = new Set();
|
|
180
|
+
seenByKind.set(identity.kind, seen);
|
|
181
|
+
}
|
|
182
|
+
if (seen.has(identity.value)) {
|
|
183
|
+
duplicateCandidates.push(index);
|
|
184
|
+
} else {
|
|
185
|
+
seen.add(identity.value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
duplicateCandidates.sort((left, right) => left - right);
|
|
190
|
+
for (const index of duplicateCandidates) {
|
|
191
|
+
if (kept <= limit) break;
|
|
192
|
+
dropped.add(index);
|
|
193
|
+
kept -= 1;
|
|
194
|
+
duplicates += 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (let index = 0; index < refs.length && kept > limit; index += 1) {
|
|
198
|
+
if (dropped.has(index)) continue;
|
|
199
|
+
dropped.add(index);
|
|
200
|
+
kept -= 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let historicalOmitted = 0;
|
|
204
|
+
let currentOmitted = 0;
|
|
205
|
+
for (const index of dropped) {
|
|
206
|
+
if (refs[index].itemIndex >= currentInputStart) currentOmitted += 1;
|
|
207
|
+
else historicalOmitted += 1;
|
|
208
|
+
}
|
|
209
|
+
beforeMutate({ historicalOmitted, currentOmitted });
|
|
210
|
+
applyDrops(records, refs, dropped, limit);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
total: refs.length,
|
|
214
|
+
kept,
|
|
215
|
+
omitted: dropped.size,
|
|
216
|
+
duplicates,
|
|
217
|
+
historicalOmitted,
|
|
218
|
+
currentOmitted,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { responses as copilotResponses } from "./copilot.mjs";
|
|
2
2
|
import { httpError } from "./http-transport.mjs";
|
|
3
|
+
import { debugLog } from "./log.mjs";
|
|
3
4
|
import { materializeResponseHistory, rememberResponseHistoryNode } from "./response-history.mjs";
|
|
5
|
+
import { enforceResponsesImageLimit } from "./responses-image-limit.mjs";
|
|
4
6
|
import { status } from "./status.mjs";
|
|
5
7
|
|
|
6
8
|
function cloneJson(value) {
|
|
@@ -163,13 +165,26 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
|
163
165
|
const body = mutate ? reqBody : cloneJson(reqBody);
|
|
164
166
|
const currentInputItems = responsesInputItems(body.input, { clone: !mutate });
|
|
165
167
|
const previousId = body.previous_response_id;
|
|
168
|
+
let historyItems = [];
|
|
166
169
|
|
|
167
170
|
if (previousId !== undefined && previousId !== null) {
|
|
168
|
-
|
|
171
|
+
historyItems = materializeResponseHistory(previousId);
|
|
172
|
+
body.input = [...historyItems, ...currentInputItems];
|
|
169
173
|
} else {
|
|
170
174
|
body.input = currentInputItems;
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
let historyInputItems = currentInputItems;
|
|
178
|
+
const imageLimit = enforceResponsesImageLimit(body.input, {
|
|
179
|
+
currentInputStart: historyItems.length,
|
|
180
|
+
beforeMutate: ({ currentOmitted }) => {
|
|
181
|
+
if (currentOmitted > 0) historyInputItems = cloneJson(currentInputItems);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
if (imageLimit.omitted > 0) {
|
|
185
|
+
debugLog(`responses image window total=${imageLimit.total} kept=${imageLimit.kept} omitted=${imageLimit.omitted} duplicates=${imageLimit.duplicates} historical=${imageLimit.historicalOmitted} current=${imageLimit.currentOmitted}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
173
188
|
delete body.previous_response_id;
|
|
174
189
|
delete body.store;
|
|
175
190
|
if (Array.isArray(body.tools)) {
|
|
@@ -177,13 +192,13 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
|
177
192
|
if (!body.tools.length) delete body.tools;
|
|
178
193
|
}
|
|
179
194
|
stripInternalResponsesInputFields(body.input);
|
|
180
|
-
stripInternalResponsesInputFields(
|
|
195
|
+
stripInternalResponsesInputFields(historyInputItems);
|
|
181
196
|
|
|
182
197
|
return {
|
|
183
198
|
body,
|
|
184
199
|
inputItems: body.input,
|
|
185
200
|
historyParentId: previousId ?? null,
|
|
186
|
-
historyInputItems
|
|
201
|
+
historyInputItems,
|
|
187
202
|
takeHistoryOwnership: mutate,
|
|
188
203
|
};
|
|
189
204
|
}
|