codex-copilot-dx 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/adapter.mjs +89 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Use Codex Desktop app with your GitHub Copilot subscription",
5
5
  "type": "module",
6
6
  "bin": {
package/src/adapter.mjs CHANGED
@@ -125,6 +125,13 @@ function sendJsonError(res, err, fallbackStatus = 400) {
125
125
  res.end(JSON.stringify({ error: err?.message || "Request failed" }));
126
126
  }
127
127
 
128
+ function sendUpstreamError(res, resp, text) {
129
+ if (!res.headersSent) {
130
+ res.writeHead(resp.status || 502, { "Content-Type": resp.headers?.get("content-type") || "application/json" });
131
+ }
132
+ res.end(text || JSON.stringify({ error: "Upstream request failed" }));
133
+ }
134
+
128
135
  export function writeOrDrain(res, chunk) {
129
136
  if (res.destroyed || res.writableEnded) return Promise.resolve(false);
130
137
  if (res.write(chunk)) return Promise.resolve(true);
@@ -180,6 +187,81 @@ function responsesOutputItems(output) {
180
187
  return cloneJson(output.filter((item) => item?.type === "message" || item?.type === "function_call"));
181
188
  }
182
189
 
190
+ function stripEncryptedReasoningValue(value, state) {
191
+ if (Array.isArray(value)) {
192
+ return value.map((item) => stripEncryptedReasoningValue(item, state));
193
+ }
194
+
195
+ if (value && typeof value === "object") {
196
+ const out = {};
197
+ for (const [key, child] of Object.entries(value)) {
198
+ if (key === "encrypted_content") {
199
+ state.changed = true;
200
+ continue;
201
+ }
202
+ out[key] = stripEncryptedReasoningValue(child, state);
203
+ }
204
+ return out;
205
+ }
206
+
207
+ return value;
208
+ }
209
+
210
+ function isEncryptedReasoningInputItem(item) {
211
+ return item && typeof item === "object"
212
+ && (item.type === "reasoning" || Object.prototype.hasOwnProperty.call(item, "encrypted_content"));
213
+ }
214
+
215
+ export function sanitizeEncryptedReasoningRequest(reqContext) {
216
+ const state = { changed: false };
217
+ let body = cloneJson(reqContext.body);
218
+ if (Array.isArray(body.input)) {
219
+ const input = [];
220
+ for (const item of body.input) {
221
+ if (isEncryptedReasoningInputItem(item)) {
222
+ state.changed = true;
223
+ continue;
224
+ }
225
+ input.push(stripEncryptedReasoningValue(item, state));
226
+ }
227
+ body.input = input;
228
+ } else {
229
+ body = stripEncryptedReasoningValue(body, state);
230
+ }
231
+ if (!state.changed) return null;
232
+ return {
233
+ ...reqContext,
234
+ body,
235
+ inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
236
+ };
237
+ }
238
+
239
+ export function isEncryptedContentVerificationError(statusCode, text) {
240
+ if (statusCode < 400 || !text) return false;
241
+ const lower = String(text).toLowerCase();
242
+ return lower.includes("encrypted content")
243
+ && lower.includes("could not be verified")
244
+ && (lower.includes("could not be decrypted") || lower.includes("could not be parsed"));
245
+ }
246
+
247
+ export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
248
+ let resp = await upstream(reqContext.body, { signal: options.signal });
249
+ if (resp.ok) return { resp, reqContext };
250
+
251
+ const errorText = await resp.text();
252
+ if (!isEncryptedContentVerificationError(resp.status, errorText)) {
253
+ return { resp, reqContext, errorText };
254
+ }
255
+
256
+ const retryContext = sanitizeEncryptedReasoningRequest(reqContext);
257
+ if (!retryContext) return { resp, reqContext, errorText };
258
+
259
+ console.warn(status("warn", "encrypted reasoning rejected by upstream; retrying without encrypted reasoning"));
260
+ resp = await upstream(retryContext.body, { signal: options.signal });
261
+ if (resp.ok) return { resp, reqContext: retryContext };
262
+ return { resp, reqContext: retryContext, errorText: await resp.text() };
263
+ }
264
+
183
265
  export function clearResponseHistoryForTests() {
184
266
  responseHistories.clear();
185
267
  }
@@ -256,7 +338,13 @@ function readSseEvents(buffer, onData) {
256
338
  }
257
339
 
258
340
  async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
259
- const resp = await upstream(reqContext.body, { signal: options.signal });
341
+ const opened = await openCopilotResponse(reqContext, upstream, options);
342
+ const { resp, errorText } = opened;
343
+ reqContext = opened.reqContext;
344
+ if (errorText !== undefined) {
345
+ sendUpstreamError(res, resp, errorText);
346
+ return;
347
+ }
260
348
 
261
349
  if (reqContext.body.stream) {
262
350
  res.writeHead(resp.status, {