codex-copilot-dx 0.6.12 → 0.6.14

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.
@@ -40,12 +40,6 @@ const PM_RELAY_ROUTE_NAMES = Object.freeze([
40
40
  "pm_chat_completions",
41
41
  ]);
42
42
 
43
- function selectRecipe(metadata, recipes) {
44
- return recipes.find((recipe) => recipe.version === String(metadata.version)
45
- && recipe.build === String(metadata.build)
46
- && recipe.bundleIdentifier === String(metadata.bundleIdentifier));
47
- }
48
-
49
43
  function safeMessage(error) {
50
44
  return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
51
45
  }
@@ -169,21 +163,20 @@ export async function inspectPmStudioStatus({
169
163
  build: String(metadata.build),
170
164
  bundleIdentifier: String(metadata.bundleIdentifier),
171
165
  };
172
- let recipe = selectRecipe(normalized, recipes);
166
+ let recipe = null;
173
167
  let compatibilityError = null;
174
- if (!recipe) {
175
- try {
176
- recipe = resolveCompatibleRecipe({
177
- appPath,
178
- home,
179
- backupRoot,
180
- metadata: normalized,
181
- recipes,
182
- operations,
183
- });
184
- } catch (error) {
185
- compatibilityError = error;
186
- }
168
+ try {
169
+ recipe = resolveCompatibleRecipe({
170
+ appPath,
171
+ home,
172
+ backupRoot,
173
+ metadata: normalized,
174
+ recipes,
175
+ operations,
176
+ inspectApp,
177
+ });
178
+ } catch (error) {
179
+ compatibilityError = error;
187
180
  }
188
181
  if (recipe) {
189
182
  app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
@@ -272,8 +265,8 @@ function appStatusItem(result, commandName) {
272
265
  if (app.state === "legacy") {
273
266
  return { kind: "err", component: "App patch", detail: `${version}; legacy global-origin patch`, message: `PM Studio ${version} has a legacy global-origin patch; run ${commandName} pms setup to migrate` };
274
267
  }
275
- if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; supported, not patched`, message: `PM Studio ${version} is supported but not patched; run ${commandName} pms setup` };
276
- if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; compatibility unverified`, message: `PM Studio ${version} compatibility cannot be safely verified; no files will be changed` };
268
+ if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; compatible structure, not patched`, message: `PM Studio ${version} has a compatible local patch structure but is not patched; run ${commandName} pms setup` };
269
+ if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; structure incompatible`, message: `PM Studio ${version} does not expose one uniquely compatible patch structure; no files will be changed` };
277
270
  if (app.state === "drift" && app.patchRecipeMatched) {
278
271
  return { kind: "err", component: "App patch", detail: `${version}; patch record drift`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}` };
279
272
  }
@@ -38,6 +38,23 @@ export function responsesBodyUsesCustomTools(body) {
38
38
  && body.input.some((item) => item?.type === "custom_tool_call" || item?.type === "custom_tool_call_output");
39
39
  }
40
40
 
41
+ function chatCompatibilityError(kind, itemType) {
42
+ const type = typeof itemType === "string" && itemType ? itemType : "unknown";
43
+ const message = `Responses ${kind} type "${type}" cannot be represented by Chat Completions`;
44
+ const error = new Error(message);
45
+ error.statusCode = 400;
46
+ error.code = "ccdx_responses_chat_incompatible";
47
+ error.jsonBody = {
48
+ error: {
49
+ message,
50
+ type: "invalid_request_error",
51
+ code: error.code,
52
+ item_type: type,
53
+ },
54
+ };
55
+ return error;
56
+ }
57
+
41
58
  export function responsesToChat(body) {
42
59
  const messages = [];
43
60
  if (body.instructions) messages.push({ role: "system", content: body.instructions });
@@ -65,19 +82,36 @@ export function responsesToChat(body) {
65
82
  if (typeof body.input === "string") {
66
83
  messages.push({ role: "user", content: body.input });
67
84
  } else if (Array.isArray(body.input)) {
68
- for (const item of body.input) {
69
- if (item.type === "message") {
85
+ for (let index = 0; index < body.input.length; index += 1) {
86
+ const item = body.input[index];
87
+ const isEasyMessage = item?.type === undefined
88
+ && ["user", "assistant", "system", "developer"].includes(item?.role)
89
+ && Object.prototype.hasOwnProperty.call(item, "content");
90
+ if (item?.type === "message" || isEasyMessage) {
70
91
  messages.push({ role: item.role, content: messageContent(item.content) });
71
- } else if (["input_image", "image_url", "image"].includes(item.type)) {
92
+ } else if (["input_image", "image_url", "image"].includes(item?.type)) {
72
93
  messages.push({ role: "user", content: messageContent([item]) });
73
- } else if (item.type === "function_call") {
94
+ } else if (item?.type === "function_call") {
95
+ const toolCalls = [];
96
+ while (index < body.input.length && body.input[index]?.type === "function_call") {
97
+ const call = body.input[index];
98
+ toolCalls.push({
99
+ id: call.call_id || randomUUID(),
100
+ type: "function",
101
+ function: { name: call.name, arguments: call.arguments },
102
+ });
103
+ index += 1;
104
+ }
105
+ index -= 1;
74
106
  messages.push({
75
107
  role: "assistant",
76
108
  content: null,
77
- tool_calls: [{ id: item.call_id || randomUUID(), type: "function", function: { name: item.name, arguments: item.arguments } }],
109
+ tool_calls: toolCalls,
78
110
  });
79
- } else if (item.type === "function_call_output") {
111
+ } else if (item?.type === "function_call_output") {
80
112
  messages.push({ role: "tool", tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) });
113
+ } else {
114
+ throw chatCompatibilityError("input item", item?.type);
81
115
  }
82
116
  }
83
117
  }
@@ -90,9 +124,10 @@ export function responsesToChat(body) {
90
124
  if (maxTok !== undefined) chatReq.max_completion_tokens = maxTok;
91
125
 
92
126
  if (body.tools?.length) {
127
+ const unsupportedTool = body.tools.find((tool) => tool?.type !== "function");
128
+ if (unsupportedTool) throw chatCompatibilityError("tool", unsupportedTool?.type);
93
129
  chatReq.tools = body.tools
94
130
  .map((tool) => {
95
- if (tool?.type !== "function") return null;
96
131
  if (tool.function?.name) return cloneJson(tool);
97
132
  const fn = {
98
133
  name: tool.name,
@@ -112,6 +147,8 @@ export function responsesToChat(body) {
112
147
  chatReq.tool_choice = body.tool_choice;
113
148
  } else if (body.tool_choice?.type === "function" && body.tool_choice.name) {
114
149
  chatReq.tool_choice = { type: "function", function: { name: body.tool_choice.name } };
150
+ } else {
151
+ throw chatCompatibilityError("tool choice", body.tool_choice?.type);
115
152
  }
116
153
  }
117
154
  if (body.parallel_tool_calls !== undefined) chatReq.parallel_tool_calls = body.parallel_tool_calls;
@@ -1,10 +1,6 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { httpError } from "./http-transport.mjs";
3
2
 
4
3
  const COMPACTION_TRIGGER_TYPE = "compaction_trigger";
5
- const RETAINED_MESSAGE_ROLES = new Set(["user", "assistant", "developer", "system"]);
6
- const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
7
- const APPROX_BYTES_PER_TOKEN = 4;
8
4
 
9
5
  function compactionError(message) {
10
6
  const error = httpError(message, 502);
@@ -19,47 +15,6 @@ function compactionError(message) {
19
15
  return error;
20
16
  }
21
17
 
22
- function normalizeMessageContent(content) {
23
- if (typeof content === "string") return [{ type: "input_text", text: content }];
24
- if (!Array.isArray(content)) return [];
25
- return content.map((part) => {
26
- if (!part || typeof part !== "object") return part;
27
- return part.type === "output_text"
28
- ? { ...part, type: "input_text" }
29
- : structuredClone(part);
30
- });
31
- }
32
-
33
- function approximateTextTokens(content) {
34
- return content.reduce((total, part) => {
35
- if (part?.type !== "input_text" || typeof part.text !== "string") return total;
36
- return total + Math.ceil(Buffer.byteLength(part.text) / APPROX_BYTES_PER_TOKEN);
37
- }, 0);
38
- }
39
-
40
- function retainedMessages(inputItems) {
41
- const kept = [];
42
- let usedTokens = 0;
43
- for (let index = inputItems.length - 1; index >= 0; index -= 1) {
44
- const item = inputItems[index];
45
- if (item?.type !== "message" || !RETAINED_MESSAGE_ROLES.has(item.role)) continue;
46
-
47
- const content = normalizeMessageContent(item.content);
48
- usedTokens += Math.max(approximateTextTokens(content), 1);
49
- if (usedTokens > RETAINED_MESSAGE_TOKEN_BUDGET && kept.length > 0) break;
50
-
51
- kept.push({
52
- type: "message",
53
- id: `msg_${randomUUID().replaceAll("-", "")}`,
54
- status: item.status ?? "completed",
55
- role: item.role,
56
- content,
57
- ...(item.phase !== undefined ? { phase: structuredClone(item.phase) } : {}),
58
- });
59
- }
60
- return kept.reverse();
61
- }
62
-
63
18
  export function prepareResponsesCompactionRequest(reqContext) {
64
19
  const input = Array.isArray(reqContext?.body?.input) ? reqContext.body.input : [];
65
20
  const compactionInputItems = compactionInputWithoutTrigger(input);
@@ -74,7 +29,7 @@ export function compactionInputWithoutTrigger(input) {
74
29
  : [];
75
30
  }
76
31
 
77
- export function createResponsesCompactionResult(inputItems, generated) {
32
+ export function createResponsesCompactionResult(_inputItems, generated) {
78
33
  if (!generated || typeof generated !== "object" || !generated.id || !Array.isArray(generated.output)) {
79
34
  throw compactionError("Copilot compaction returned an invalid response envelope");
80
35
  }
@@ -95,10 +50,6 @@ export function createResponsesCompactionResult(inputItems, generated) {
95
50
  return {
96
51
  ...generated,
97
52
  object: "response.compaction",
98
- output: [
99
- ...retainedMessages(Array.isArray(inputItems) ? inputItems : []),
100
- ...compactionItems,
101
- ],
102
53
  };
103
54
  }
104
55
 
@@ -81,15 +81,38 @@ function inspectResponseSseEvent(state, eventName, data) {
81
81
  state.completed = { response, event };
82
82
  }
83
83
 
84
- function readSseEvents(buffer, onEvent) {
85
- while (true) {
86
- const match = buffer.match(/\r?\n\r?\n/);
87
- if (!match) return buffer;
88
- const chunk = buffer.slice(0, match.index);
89
- if (Buffer.byteLength(chunk) > MAX_SSE_BUFFER_BYTES) {
84
+ function createResponseSseInspector(state) {
85
+ let eventBuffer = Buffer.allocUnsafe(Math.min(4096, MAX_SSE_BUFFER_BYTES + 4));
86
+ let eventLength = 0;
87
+ let previous1 = -1;
88
+ let previous2 = -1;
89
+ let previous3 = -1;
90
+
91
+ const ensureCapacity = (required) => {
92
+ if (required > MAX_SSE_BUFFER_BYTES + 4) {
93
+ throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
94
+ }
95
+ if (required <= eventBuffer.length) return;
96
+ let capacity = eventBuffer.length;
97
+ while (capacity < required) capacity = Math.min(MAX_SSE_BUFFER_BYTES + 4, Math.max(capacity * 2, required));
98
+ const next = Buffer.allocUnsafe(capacity);
99
+ eventBuffer.copy(next, 0, 0, eventLength);
100
+ eventBuffer = next;
101
+ };
102
+
103
+ const append = (bytes) => {
104
+ if (!bytes.byteLength) return;
105
+ ensureCapacity(eventLength + bytes.byteLength);
106
+ eventBuffer.set(bytes, eventLength);
107
+ eventLength += bytes.byteLength;
108
+ };
109
+
110
+ const inspectEvent = (delimiterLength) => {
111
+ const contentLength = eventLength - delimiterLength;
112
+ if (contentLength > MAX_SSE_BUFFER_BYTES) {
90
113
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
91
114
  }
92
- buffer = buffer.slice(match.index + match[0].length);
115
+ const chunk = eventBuffer.subarray(0, contentLength).toString("utf8");
93
116
  const lines = chunk.split(/\r?\n/);
94
117
  const eventName = lines
95
118
  .find((line) => line.startsWith("event:"))
@@ -99,8 +122,73 @@ function readSseEvents(buffer, onEvent) {
99
122
  .filter((line) => line.startsWith("data:"))
100
123
  .map((line) => line.slice(5).trimStart())
101
124
  .join("\n");
102
- if (data) onEvent(eventName, data);
125
+ if (data) inspectResponseSseEvent(state, eventName, data);
126
+ eventLength = 0;
127
+ previous1 = -1;
128
+ previous2 = -1;
129
+ previous3 = -1;
130
+ };
131
+
132
+ return {
133
+ push(value) {
134
+ let segmentStart = 0;
135
+ for (let index = 0; index < value.byteLength; index += 1) {
136
+ const byte = value[index];
137
+ let delimiterLength = 0;
138
+ if (byte === 0x0a && previous1 === 0x0a) {
139
+ delimiterLength = previous2 === 0x0d ? 3 : 2;
140
+ } else if (byte === 0x0a && previous1 === 0x0d && previous2 === 0x0a) {
141
+ delimiterLength = previous3 === 0x0d ? 4 : 3;
142
+ }
143
+ if (delimiterLength) {
144
+ append(value.subarray(segmentStart, index + 1));
145
+ inspectEvent(delimiterLength);
146
+ segmentStart = index + 1;
147
+ if (state.sawTerminal) return index + 1;
148
+ continue;
149
+ }
150
+ previous3 = previous2;
151
+ previous2 = previous1;
152
+ previous1 = byte;
153
+ }
154
+ append(value.subarray(segmentStart));
155
+ if (eventLength > MAX_SSE_BUFFER_BYTES + 3) {
156
+ throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
157
+ }
158
+ return null;
159
+ },
160
+ };
161
+ }
162
+
163
+ function invalidResponsesResponse(message) {
164
+ const error = httpError(message, 502);
165
+ error.code = "ccdx_invalid_responses_response";
166
+ error.jsonBody = {
167
+ error: {
168
+ message,
169
+ type: "upstream_error",
170
+ code: error.code,
171
+ },
172
+ };
173
+ return error;
174
+ }
175
+
176
+ function parseSuccessfulResponsesResult(text) {
177
+ let response;
178
+ try {
179
+ response = JSON.parse(text);
180
+ } catch {
181
+ throw invalidResponsesResponse("Copilot Responses returned invalid JSON");
182
+ }
183
+ if (!response
184
+ || typeof response !== "object"
185
+ || Array.isArray(response)
186
+ || typeof response.id !== "string"
187
+ || response.id.length === 0
188
+ || !Array.isArray(response.output)) {
189
+ throw invalidResponsesResponse("Copilot Responses returned an invalid response envelope");
103
190
  }
191
+ return response;
104
192
  }
105
193
 
106
194
  function storeCompletedResponse(reqContext, completed) {
@@ -138,35 +226,26 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
138
226
  Connection: "keep-alive",
139
227
  });
140
228
  const reader = resp.body.getReader();
141
- const decoder = new TextDecoder();
142
- let buffer = "";
143
229
  const streamState = {
144
230
  completed: null,
145
231
  sawTerminal: false,
146
232
  sawOutput: false,
147
233
  toolArgumentGuard: new ToolArgumentDeltaGuard(),
148
234
  };
235
+ const inspector = createResponseSseInspector(streamState);
149
236
  try {
150
237
  while (true) {
151
238
  const { done, value } = await reader.read();
152
239
  if (done) {
153
- buffer += decoder.decode();
154
- if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
155
- throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
156
- }
157
- readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
158
240
  if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
159
241
  storeCompletedResponse(reqContext, streamState.completed);
160
242
  res.end();
161
243
  return { successful: Boolean(streamState.completed), compacted: false };
162
244
  }
163
245
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
164
- buffer += decoder.decode(value, { stream: true });
165
- buffer = readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
166
- if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
167
- throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
168
- }
169
- if (!await writeOrDrain(res, value)) return;
246
+ const terminalOffset = inspector.push(value);
247
+ const forwarded = terminalOffset === null ? value : value.subarray(0, terminalOffset);
248
+ if (forwarded.byteLength > 0 && !await writeOrDrain(res, forwarded)) return;
170
249
  if (streamState.sawTerminal) {
171
250
  storeCompletedResponse(reqContext, streamState.completed);
172
251
  res.end();
@@ -202,24 +281,26 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
202
281
  res.end(JSON.stringify(response));
203
282
  return { successful: true, compacted: true };
204
283
  }
284
+ if (resp.ok) {
285
+ const response = parseSuccessfulResponsesResult(data);
286
+ rememberResponseHistory(reqContext, response);
287
+ recordResponsesUsage({
288
+ surface: reqContext.surface,
289
+ mode: "json",
290
+ model: reqContext.body?.model,
291
+ response,
292
+ event: response,
293
+ });
294
+ res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
295
+ contentType: "application/json",
296
+ }));
297
+ res.end(data);
298
+ return { successful: true, compacted: false };
299
+ }
205
300
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
206
301
  contentType: "application/json",
207
302
  }));
208
303
  res.end(data);
209
- if (resp.ok) {
210
- try {
211
- const response = JSON.parse(data);
212
- rememberResponseHistory(reqContext, response);
213
- recordResponsesUsage({
214
- surface: reqContext.surface,
215
- mode: "json",
216
- model: reqContext.body?.model,
217
- response,
218
- event: response,
219
- });
220
- return { successful: true, compacted: false };
221
- } catch {}
222
- }
223
304
  return { successful: false, compacted: false };
224
305
  }
225
306
  }
@@ -46,7 +46,7 @@ export function stripInternalResponsesInputFields(inputItems, { assertActive } =
46
46
 
47
47
  function responsesOutputItems(output) {
48
48
  if (!Array.isArray(output)) return [];
49
- return output.filter((item) => ["message", "function_call", "custom_tool_call"].includes(item?.type));
49
+ return output.filter((item) => item && typeof item === "object");
50
50
  }
51
51
 
52
52
  function isCompactedResponsesOutput(output) {
@@ -62,8 +62,7 @@ function isSuccessfulCompactionResponse(response) {
62
62
  }
63
63
 
64
64
  function replayableResponsesOutputItems(output) {
65
- if (!Array.isArray(output)) return [];
66
- return output.filter((item) => item && typeof item === "object");
65
+ return responsesOutputItems(output);
67
66
  }
68
67
 
69
68
  function isEncryptedContentPart(value) {
package/src/stream.mjs CHANGED
@@ -4,33 +4,47 @@ import { loadRuntimeConfig } from "./runtime-config.mjs";
4
4
 
5
5
  const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
6
6
 
7
- function assertSseBufferLimit(buffer, maxBufferBytes) {
8
- if (Buffer.byteLength(buffer) > maxBufferBytes) {
7
+ function appendLineFragment(parts, fragment, currentBytes, maxBufferBytes) {
8
+ if (!fragment) return currentBytes;
9
+ const nextBytes = currentBytes + Buffer.byteLength(fragment);
10
+ if (nextBytes > maxBufferBytes) {
9
11
  throw new Error(`Upstream SSE buffer exceeds ${maxBufferBytes} bytes`);
10
12
  }
13
+ parts.push(fragment);
14
+ return nextBytes;
11
15
  }
12
16
 
13
17
  export async function* webStreamLines(response, { onChunk, maxBufferBytes = MAX_SSE_BUFFER_BYTES } = {}) {
14
18
  const reader = response.body.getReader();
15
19
  const decoder = new TextDecoder();
16
- let buf = "";
20
+ let lineParts = [];
21
+ let lineBytes = 0;
17
22
  try {
18
23
  while (true) {
19
24
  const { done, value } = await reader.read();
20
25
  if (done) break;
21
26
  onChunk?.(value);
22
- buf += decoder.decode(value, { stream: true });
23
- const lines = buf.split("\n");
24
- buf = lines.pop();
25
- assertSseBufferLimit(buf, maxBufferBytes);
26
- for (const line of lines) {
27
- assertSseBufferLimit(line, maxBufferBytes);
28
- yield line.replace(/\r$/, "");
27
+ const decoded = decoder.decode(value, { stream: true });
28
+ let start = 0;
29
+ while (start < decoded.length) {
30
+ const newline = decoded.indexOf("\n", start);
31
+ if (newline === -1) {
32
+ lineBytes = appendLineFragment(lineParts, decoded.slice(start), lineBytes, maxBufferBytes);
33
+ break;
34
+ }
35
+ lineBytes = appendLineFragment(lineParts, decoded.slice(start, newline), lineBytes, maxBufferBytes);
36
+ const line = lineParts.length > 1 ? lineParts.join("") : (lineParts[0] || "");
37
+ yield line.endsWith("\r") ? line.slice(0, -1) : line;
38
+ lineParts = [];
39
+ lineBytes = 0;
40
+ start = newline + 1;
29
41
  }
30
42
  }
31
- buf += decoder.decode();
32
- assertSseBufferLimit(buf, maxBufferBytes);
33
- if (buf.length > 0) yield buf.replace(/\r$/, "");
43
+ lineBytes = appendLineFragment(lineParts, decoder.decode(), lineBytes, maxBufferBytes);
44
+ if (lineParts.length > 0) {
45
+ const line = lineParts.length > 1 ? lineParts.join("") : lineParts[0];
46
+ yield line.endsWith("\r") ? line.slice(0, -1) : line;
47
+ }
34
48
  } finally {
35
49
  await reader.cancel().catch(() => {});
36
50
  reader.releaseLock();