codex-copilot-dx 0.4.41 → 0.4.43
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 +11 -5
- package/package.json +1 -1
- package/src/adapter.mjs +1 -0
- package/src/copilot.mjs +32 -5
- package/src/http-transport.mjs +1 -1
- package/src/image-optimization.mjs +269 -82
- package/src/responses-bridge.mjs +31 -11
- package/src/responses-byte-budget.mjs +314 -0
- package/src/responses-chat-payload.mjs +128 -0
- package/src/responses-content.mjs +86 -0
- package/src/responses-handler.mjs +39 -6
- package/src/responses-image-limit.mjs +209 -0
- package/src/responses-request.mjs +61 -9
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { readResponsesImagePart, readResponsesToolOutputParts } from "./responses-content.mjs";
|
|
2
|
+
|
|
3
|
+
export const MAX_UPSTREAM_IMAGES = 50;
|
|
4
|
+
|
|
5
|
+
function isImagePart(part) {
|
|
6
|
+
return part && typeof part === "object"
|
|
7
|
+
&& ["input_image", "image", "image_url"].includes(part.type);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function imageIdentity(part) {
|
|
11
|
+
if (!isImagePart(part)) return null;
|
|
12
|
+
|
|
13
|
+
const inlineIdentity = readResponsesImagePart(part)?.identity;
|
|
14
|
+
if (inlineIdentity) return { kind: "inline:data", value: inlineIdentity };
|
|
15
|
+
|
|
16
|
+
if (part.type === "image") {
|
|
17
|
+
const source = part.source;
|
|
18
|
+
if (typeof source?.url === "string") {
|
|
19
|
+
return { kind: `url:${part.detail || ""}`, value: source.url };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const raw = part.image_url ?? part.url;
|
|
24
|
+
const url = typeof raw === "string" ? raw : raw?.url;
|
|
25
|
+
if (typeof url === "string") {
|
|
26
|
+
const detail = part.detail ?? raw?.detail ?? "";
|
|
27
|
+
return { kind: `url:${detail}`, value: url };
|
|
28
|
+
}
|
|
29
|
+
if (typeof part.file_id === "string") {
|
|
30
|
+
return { kind: "file", value: part.file_id };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function markerPart(record, maxImages) {
|
|
36
|
+
const text = `[Earlier image omitted to stay within the upstream ${maxImages}-image request limit.]`;
|
|
37
|
+
if (record.kind === "message") {
|
|
38
|
+
return {
|
|
39
|
+
type: record.role === "assistant" ? "output_text" : "input_text",
|
|
40
|
+
text,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { type: record.usesAnthropicParts ? "text" : "input_text", text };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function addPartContainer(records, refs, parts, options) {
|
|
47
|
+
if (!Array.isArray(parts)) return;
|
|
48
|
+
const record = {
|
|
49
|
+
parts,
|
|
50
|
+
kind: options.kind,
|
|
51
|
+
role: options.role,
|
|
52
|
+
commit: options.commit,
|
|
53
|
+
usesAnthropicParts: false,
|
|
54
|
+
};
|
|
55
|
+
let found = false;
|
|
56
|
+
for (let partIndex = 0; partIndex < parts.length; partIndex += 1) {
|
|
57
|
+
const part = parts[partIndex];
|
|
58
|
+
if (!isImagePart(part)) continue;
|
|
59
|
+
found = true;
|
|
60
|
+
if (part.type === "image") record.usesAnthropicParts = true;
|
|
61
|
+
refs.push({
|
|
62
|
+
record,
|
|
63
|
+
partIndex,
|
|
64
|
+
itemIndex: options.itemIndex,
|
|
65
|
+
identity: imageIdentity(part),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (found) records.push(record);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectImages(inputItems) {
|
|
72
|
+
const records = [];
|
|
73
|
+
const refs = [];
|
|
74
|
+
const topLevelRecord = {
|
|
75
|
+
parts: inputItems,
|
|
76
|
+
kind: "top-level",
|
|
77
|
+
role: "user",
|
|
78
|
+
commit: null,
|
|
79
|
+
usesAnthropicParts: false,
|
|
80
|
+
};
|
|
81
|
+
let hasTopLevelImages = false;
|
|
82
|
+
|
|
83
|
+
for (let itemIndex = 0; itemIndex < inputItems.length; itemIndex += 1) {
|
|
84
|
+
const item = inputItems[itemIndex];
|
|
85
|
+
if (!item || typeof item !== "object") continue;
|
|
86
|
+
|
|
87
|
+
if (isImagePart(item)) {
|
|
88
|
+
hasTopLevelImages = true;
|
|
89
|
+
refs.push({
|
|
90
|
+
record: topLevelRecord,
|
|
91
|
+
partIndex: itemIndex,
|
|
92
|
+
itemIndex,
|
|
93
|
+
identity: imageIdentity(item),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (item.type === "message") {
|
|
98
|
+
addPartContainer(records, refs, item.content, {
|
|
99
|
+
kind: "message",
|
|
100
|
+
role: item.role,
|
|
101
|
+
itemIndex,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const toolOutput = readResponsesToolOutputParts(item);
|
|
106
|
+
if (!toolOutput) continue;
|
|
107
|
+
addPartContainer(records, refs, toolOutput.parts, {
|
|
108
|
+
kind: "tool-output",
|
|
109
|
+
itemIndex,
|
|
110
|
+
commit: toolOutput.commit,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (hasTopLevelImages) records.push(topLevelRecord);
|
|
115
|
+
return { records, refs };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function applyDrops(records, refs, dropped, maxImages) {
|
|
119
|
+
const droppedByRecord = new Map();
|
|
120
|
+
for (const refIndex of dropped) {
|
|
121
|
+
const ref = refs[refIndex];
|
|
122
|
+
let indexes = droppedByRecord.get(ref.record);
|
|
123
|
+
if (!indexes) {
|
|
124
|
+
indexes = new Set();
|
|
125
|
+
droppedByRecord.set(ref.record, indexes);
|
|
126
|
+
}
|
|
127
|
+
indexes.add(ref.partIndex);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const record of records) {
|
|
131
|
+
const indexes = droppedByRecord.get(record);
|
|
132
|
+
if (!indexes) continue;
|
|
133
|
+
const kept = record.parts.filter((_, index) => !indexes.has(index));
|
|
134
|
+
if (!kept.length && record.kind !== "top-level") kept.push(markerPart(record, maxImages));
|
|
135
|
+
record.parts.splice(0, record.parts.length, ...kept);
|
|
136
|
+
record.commit?.();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function enforceResponsesImageLimit(inputItems, {
|
|
141
|
+
currentInputStart = 0,
|
|
142
|
+
maxImages = MAX_UPSTREAM_IMAGES,
|
|
143
|
+
beforeMutate = () => {},
|
|
144
|
+
} = {}) {
|
|
145
|
+
const limit = Number.isFinite(maxImages) && maxImages > 0
|
|
146
|
+
? Math.floor(maxImages)
|
|
147
|
+
: MAX_UPSTREAM_IMAGES;
|
|
148
|
+
if (!Array.isArray(inputItems)) {
|
|
149
|
+
return { total: 0, kept: 0, omitted: 0, duplicates: 0, historicalOmitted: 0, currentOmitted: 0 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { records, refs } = collectImages(inputItems);
|
|
153
|
+
if (refs.length <= limit) {
|
|
154
|
+
return { total: refs.length, kept: refs.length, omitted: 0, duplicates: 0, historicalOmitted: 0, currentOmitted: 0 };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const dropped = new Set();
|
|
158
|
+
const seenByKind = new Map();
|
|
159
|
+
const duplicateCandidates = [];
|
|
160
|
+
let kept = refs.length;
|
|
161
|
+
let duplicates = 0;
|
|
162
|
+
|
|
163
|
+
for (let index = refs.length - 1; index >= 0; index -= 1) {
|
|
164
|
+
const identity = refs[index].identity;
|
|
165
|
+
if (!identity) continue;
|
|
166
|
+
let seen = seenByKind.get(identity.kind);
|
|
167
|
+
if (!seen) {
|
|
168
|
+
seen = new Set();
|
|
169
|
+
seenByKind.set(identity.kind, seen);
|
|
170
|
+
}
|
|
171
|
+
if (seen.has(identity.value)) {
|
|
172
|
+
duplicateCandidates.push(index);
|
|
173
|
+
} else {
|
|
174
|
+
seen.add(identity.value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
duplicateCandidates.sort((left, right) => left - right);
|
|
179
|
+
for (const index of duplicateCandidates) {
|
|
180
|
+
if (kept <= limit) break;
|
|
181
|
+
dropped.add(index);
|
|
182
|
+
kept -= 1;
|
|
183
|
+
duplicates += 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (let index = 0; index < refs.length && kept > limit; index += 1) {
|
|
187
|
+
if (dropped.has(index)) continue;
|
|
188
|
+
dropped.add(index);
|
|
189
|
+
kept -= 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let historicalOmitted = 0;
|
|
193
|
+
let currentOmitted = 0;
|
|
194
|
+
for (const index of dropped) {
|
|
195
|
+
if (refs[index].itemIndex >= currentInputStart) currentOmitted += 1;
|
|
196
|
+
else historicalOmitted += 1;
|
|
197
|
+
}
|
|
198
|
+
beforeMutate({ historicalOmitted, currentOmitted });
|
|
199
|
+
applyDrops(records, refs, dropped, limit);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
total: refs.length,
|
|
203
|
+
kept,
|
|
204
|
+
omitted: dropped.size,
|
|
205
|
+
duplicates,
|
|
206
|
+
historicalOmitted,
|
|
207
|
+
currentOmitted,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -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) {
|
|
@@ -29,7 +31,24 @@ export function stripInternalResponsesInputFields(inputItems) {
|
|
|
29
31
|
|
|
30
32
|
function responsesOutputItems(output) {
|
|
31
33
|
if (!Array.isArray(output)) return [];
|
|
32
|
-
return output.filter((item) =>
|
|
34
|
+
return output.filter((item) => ["message", "function_call", "custom_tool_call"].includes(item?.type));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCompactedResponsesOutput(output) {
|
|
38
|
+
return Array.isArray(output) && output.some((item) => item?.type === "compaction");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSuccessfulCompactionResponse(response) {
|
|
42
|
+
if (!isCompactedResponsesOutput(response?.output)) return false;
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(response, "status") && response.status !== "completed") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return response.object === "response.compaction" || response.status === "completed";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function replayableResponsesOutputItems(output) {
|
|
50
|
+
if (!Array.isArray(output)) return [];
|
|
51
|
+
return output.filter((item) => item && typeof item === "object");
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
function stripEncryptedReasoningValue(value, state) {
|
|
@@ -60,16 +79,21 @@ function isEncryptedReasoningInputItem(item) {
|
|
|
60
79
|
export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
61
80
|
const state = { changed: false };
|
|
62
81
|
let body = cloneJson(reqContext.body);
|
|
82
|
+
let currentInputStart = reqContext.currentInputStart;
|
|
63
83
|
if (Array.isArray(body.input)) {
|
|
64
84
|
const input = [];
|
|
65
|
-
|
|
85
|
+
let retainedBeforeCurrent = 0;
|
|
86
|
+
for (let index = 0; index < body.input.length; index += 1) {
|
|
87
|
+
const item = body.input[index];
|
|
66
88
|
if (isEncryptedReasoningInputItem(item)) {
|
|
67
89
|
state.changed = true;
|
|
68
90
|
continue;
|
|
69
91
|
}
|
|
70
92
|
input.push(stripEncryptedReasoningValue(item, state));
|
|
93
|
+
if (Number.isFinite(currentInputStart) && index < currentInputStart) retainedBeforeCurrent += 1;
|
|
71
94
|
}
|
|
72
95
|
body.input = input;
|
|
96
|
+
if (Number.isFinite(currentInputStart)) currentInputStart = retainedBeforeCurrent;
|
|
73
97
|
} else {
|
|
74
98
|
body = stripEncryptedReasoningValue(body, state);
|
|
75
99
|
}
|
|
@@ -82,6 +106,7 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
|
82
106
|
return {
|
|
83
107
|
...reqContext,
|
|
84
108
|
body,
|
|
109
|
+
currentInputStart,
|
|
85
110
|
inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
|
|
86
111
|
historyInputItems,
|
|
87
112
|
};
|
|
@@ -127,8 +152,14 @@ export function sanitizeImageNamespaceCollisionRequest(reqContext) {
|
|
|
127
152
|
export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
|
|
128
153
|
let encryptedRetried = false;
|
|
129
154
|
let imageNamespaceRetried = false;
|
|
155
|
+
let payloadPrepared = false;
|
|
130
156
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
131
|
-
const resp = await upstream(reqContext.body, {
|
|
157
|
+
const resp = await upstream(reqContext.body, {
|
|
158
|
+
signal: options.signal,
|
|
159
|
+
currentInputStart: reqContext.currentInputStart,
|
|
160
|
+
payloadPrepared,
|
|
161
|
+
});
|
|
162
|
+
payloadPrepared = true;
|
|
132
163
|
if (resp.ok) return { resp, reqContext };
|
|
133
164
|
|
|
134
165
|
const errorText = await resp.text();
|
|
@@ -162,14 +193,30 @@ function isBuiltinImageTool(tool) {
|
|
|
162
193
|
export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
163
194
|
const body = mutate ? reqBody : cloneJson(reqBody);
|
|
164
195
|
const currentInputItems = responsesInputItems(body.input, { clone: !mutate });
|
|
196
|
+
const currentInputRefs = new Set(
|
|
197
|
+
currentInputItems.filter((item) => item && typeof item === "object"),
|
|
198
|
+
);
|
|
165
199
|
const previousId = body.previous_response_id;
|
|
200
|
+
let historyItems = [];
|
|
166
201
|
|
|
167
202
|
if (previousId !== undefined && previousId !== null) {
|
|
168
|
-
|
|
203
|
+
historyItems = materializeResponseHistory(previousId);
|
|
204
|
+
body.input = [...historyItems, ...currentInputItems];
|
|
169
205
|
} else {
|
|
170
206
|
body.input = currentInputItems;
|
|
171
207
|
}
|
|
172
208
|
|
|
209
|
+
let historyInputItems = currentInputItems;
|
|
210
|
+
const imageLimit = enforceResponsesImageLimit(body.input, {
|
|
211
|
+
currentInputStart: historyItems.length,
|
|
212
|
+
beforeMutate: ({ currentOmitted }) => {
|
|
213
|
+
if (currentOmitted > 0) historyInputItems = cloneJson(currentInputItems);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
if (imageLimit.omitted > 0) {
|
|
217
|
+
debugLog(`responses image window total=${imageLimit.total} kept=${imageLimit.kept} omitted=${imageLimit.omitted} duplicates=${imageLimit.duplicates} historical=${imageLimit.historicalOmitted} current=${imageLimit.currentOmitted}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
173
220
|
delete body.previous_response_id;
|
|
174
221
|
delete body.store;
|
|
175
222
|
if (Array.isArray(body.tools)) {
|
|
@@ -177,24 +224,29 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
|
177
224
|
if (!body.tools.length) delete body.tools;
|
|
178
225
|
}
|
|
179
226
|
stripInternalResponsesInputFields(body.input);
|
|
180
|
-
stripInternalResponsesInputFields(
|
|
227
|
+
stripInternalResponsesInputFields(historyInputItems);
|
|
228
|
+
const retainedCurrentInputStart = body.input.findIndex((item) => currentInputRefs.has(item));
|
|
181
229
|
|
|
182
230
|
return {
|
|
183
231
|
body,
|
|
184
232
|
inputItems: body.input,
|
|
233
|
+
currentInputStart: retainedCurrentInputStart < 0 ? body.input.length : retainedCurrentInputStart,
|
|
185
234
|
historyParentId: previousId ?? null,
|
|
186
|
-
historyInputItems
|
|
235
|
+
historyInputItems,
|
|
187
236
|
takeHistoryOwnership: mutate,
|
|
188
237
|
};
|
|
189
238
|
}
|
|
190
239
|
|
|
191
240
|
export function rememberResponseHistory(reqContext, responseJson) {
|
|
192
241
|
if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
|
|
193
|
-
const
|
|
194
|
-
const
|
|
242
|
+
const compacted = isSuccessfulCompactionResponse(responseJson);
|
|
243
|
+
const sourceInputItems = compacted ? [] : reqContext.historyInputItems || reqContext.inputItems;
|
|
244
|
+
const sourceOutputItems = compacted
|
|
245
|
+
? replayableResponsesOutputItems(responseJson.output)
|
|
246
|
+
: responsesOutputItems(responseJson.output);
|
|
195
247
|
rememberResponseHistoryNode({
|
|
196
248
|
id: responseJson.id,
|
|
197
|
-
parentId: reqContext.historyParentId,
|
|
249
|
+
parentId: compacted ? null : reqContext.historyParentId,
|
|
198
250
|
inputItems: sourceInputItems,
|
|
199
251
|
outputItems: sourceOutputItems,
|
|
200
252
|
takeOwnership: reqContext.takeHistoryOwnership,
|