codex-copilot-dx 0.4.42 → 0.4.44

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.
@@ -31,7 +31,24 @@ export function stripInternalResponsesInputFields(inputItems) {
31
31
 
32
32
  function responsesOutputItems(output) {
33
33
  if (!Array.isArray(output)) return [];
34
- return output.filter((item) => item?.type === "message" || item?.type === "function_call");
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");
35
52
  }
36
53
 
37
54
  function stripEncryptedReasoningValue(value, state) {
@@ -62,16 +79,21 @@ function isEncryptedReasoningInputItem(item) {
62
79
  export function sanitizeEncryptedReasoningRequest(reqContext) {
63
80
  const state = { changed: false };
64
81
  let body = cloneJson(reqContext.body);
82
+ let currentInputStart = reqContext.currentInputStart;
65
83
  if (Array.isArray(body.input)) {
66
84
  const input = [];
67
- for (const item of body.input) {
85
+ let retainedBeforeCurrent = 0;
86
+ for (let index = 0; index < body.input.length; index += 1) {
87
+ const item = body.input[index];
68
88
  if (isEncryptedReasoningInputItem(item)) {
69
89
  state.changed = true;
70
90
  continue;
71
91
  }
72
92
  input.push(stripEncryptedReasoningValue(item, state));
93
+ if (Number.isFinite(currentInputStart) && index < currentInputStart) retainedBeforeCurrent += 1;
73
94
  }
74
95
  body.input = input;
96
+ if (Number.isFinite(currentInputStart)) currentInputStart = retainedBeforeCurrent;
75
97
  } else {
76
98
  body = stripEncryptedReasoningValue(body, state);
77
99
  }
@@ -84,6 +106,7 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
84
106
  return {
85
107
  ...reqContext,
86
108
  body,
109
+ currentInputStart,
87
110
  inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
88
111
  historyInputItems,
89
112
  };
@@ -129,8 +152,14 @@ export function sanitizeImageNamespaceCollisionRequest(reqContext) {
129
152
  export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
130
153
  let encryptedRetried = false;
131
154
  let imageNamespaceRetried = false;
155
+ let payloadPrepared = false;
132
156
  for (let attempt = 0; attempt < 3; attempt += 1) {
133
- const resp = await upstream(reqContext.body, { signal: options.signal });
157
+ const resp = await upstream(reqContext.body, {
158
+ signal: options.signal,
159
+ currentInputStart: reqContext.currentInputStart,
160
+ payloadPrepared,
161
+ });
162
+ payloadPrepared = true;
134
163
  if (resp.ok) return { resp, reqContext };
135
164
 
136
165
  const errorText = await resp.text();
@@ -164,6 +193,9 @@ function isBuiltinImageTool(tool) {
164
193
  export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
165
194
  const body = mutate ? reqBody : cloneJson(reqBody);
166
195
  const currentInputItems = responsesInputItems(body.input, { clone: !mutate });
196
+ const currentInputRefs = new Set(
197
+ currentInputItems.filter((item) => item && typeof item === "object"),
198
+ );
167
199
  const previousId = body.previous_response_id;
168
200
  let historyItems = [];
169
201
 
@@ -193,10 +225,12 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
193
225
  }
194
226
  stripInternalResponsesInputFields(body.input);
195
227
  stripInternalResponsesInputFields(historyInputItems);
228
+ const retainedCurrentInputStart = body.input.findIndex((item) => currentInputRefs.has(item));
196
229
 
197
230
  return {
198
231
  body,
199
232
  inputItems: body.input,
233
+ currentInputStart: retainedCurrentInputStart < 0 ? body.input.length : retainedCurrentInputStart,
200
234
  historyParentId: previousId ?? null,
201
235
  historyInputItems,
202
236
  takeHistoryOwnership: mutate,
@@ -205,11 +239,14 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
205
239
 
206
240
  export function rememberResponseHistory(reqContext, responseJson) {
207
241
  if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
208
- const sourceInputItems = reqContext.historyInputItems || reqContext.inputItems;
209
- const sourceOutputItems = responsesOutputItems(responseJson.output);
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);
210
247
  rememberResponseHistoryNode({
211
248
  id: responseJson.id,
212
- parentId: reqContext.historyParentId,
249
+ parentId: compacted ? null : reqContext.historyParentId,
213
250
  inputItems: sourceInputItems,
214
251
  outputItems: sourceOutputItems,
215
252
  takeOwnership: reqContext.takeHistoryOwnership,
@@ -0,0 +1,102 @@
1
+ const DEFAULT_BLANK_TOOL_ARGUMENT_LIMIT = 20;
2
+
3
+ function streamProtocolError(message, code) {
4
+ const error = new Error(message);
5
+ error.statusCode = 502;
6
+ error.code = code;
7
+ error.jsonBody = {
8
+ error: {
9
+ message,
10
+ type: "upstream_protocol_error",
11
+ code,
12
+ },
13
+ };
14
+ return error;
15
+ }
16
+
17
+ export function invalidUpstreamStream(message, code = "upstream_stream_invalid") {
18
+ return streamProtocolError(message, code);
19
+ }
20
+
21
+ export function withChatStreamUsage(request) {
22
+ const streamOptions = request?.stream_options && typeof request.stream_options === "object"
23
+ ? request.stream_options
24
+ : {};
25
+ if (request?.stream === true && streamOptions.include_usage === true) return request;
26
+ return {
27
+ ...request,
28
+ stream: true,
29
+ stream_options: { ...streamOptions, include_usage: true },
30
+ };
31
+ }
32
+
33
+ export async function requireUpstreamEventStream(response) {
34
+ let error = null;
35
+ if (!response?.body) {
36
+ error = streamProtocolError(
37
+ "Upstream returned a successful streaming response without a body",
38
+ "upstream_stream_body_missing",
39
+ );
40
+ } else {
41
+ const contentType = response.headers?.get?.("content-type") || "";
42
+ const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
43
+ if (mediaType !== "text/event-stream") {
44
+ error = streamProtocolError(
45
+ "Upstream returned a successful streaming response with a non-SSE Content-Type",
46
+ "upstream_stream_content_type_invalid",
47
+ );
48
+ }
49
+ }
50
+
51
+ if (!error) return;
52
+ await response?.body?.cancel?.().catch(() => {});
53
+ throw error;
54
+ }
55
+
56
+ export function incompleteUpstreamStream(expectedTerminator) {
57
+ return streamProtocolError(
58
+ `Upstream stream ended before ${expectedTerminator}`,
59
+ "upstream_stream_incomplete",
60
+ );
61
+ }
62
+
63
+ export function upstreamChatStreamError(event) {
64
+ const payload = event?.error;
65
+ if (!payload || typeof payload !== "object") return null;
66
+ const type = typeof payload.type === "string" && payload.type ? `${payload.type}: ` : "";
67
+ const message = typeof payload.message === "string" && payload.message
68
+ ? payload.message.slice(0, 500)
69
+ : "Upstream stream error";
70
+ return streamProtocolError(
71
+ `Upstream Chat Completions SSE error: ${type}${message}`,
72
+ "upstream_chat_stream_error",
73
+ );
74
+ }
75
+
76
+ export class ToolArgumentDeltaGuard {
77
+ constructor(limit = DEFAULT_BLANK_TOOL_ARGUMENT_LIMIT) {
78
+ this.limit = limit;
79
+ this.whitespaceRuns = new Map();
80
+ }
81
+
82
+ observe(callIndex, delta) {
83
+ if (typeof delta !== "string") return;
84
+ const key = callIndex ?? "unknown";
85
+ let count = this.whitespaceRuns.get(key) || 0;
86
+ for (const character of delta) {
87
+ if (character === "\r" || character === "\n" || character === "\t") {
88
+ count += 1;
89
+ if (count > this.limit) {
90
+ this.whitespaceRuns.set(key, count);
91
+ throw streamProtocolError(
92
+ `Upstream sent more than ${this.limit} consecutive line-break or tab characters in tool arguments`,
93
+ "upstream_tool_arguments_stalled",
94
+ );
95
+ }
96
+ } else if (character !== " ") {
97
+ count = 0;
98
+ }
99
+ }
100
+ this.whitespaceRuns.set(key, count);
101
+ }
102
+ }
@@ -1,7 +1,8 @@
1
1
  import { writeOrDrain } from "./http-transport.mjs";
2
+ import { markStreamFailure } from "./stream-performance.mjs";
2
3
 
3
4
  function streamErrorData(protocol, error, abort) {
4
- const code = abort?.reason || "upstream_stream_error";
5
+ const code = abort?.reason || error?.code || "upstream_stream_error";
5
6
  const message = `${code}: ${error?.message || "Upstream stream failed"}`;
6
7
  if (protocol === "anthropic") {
7
8
  return { type: "error", error: { type: "api_error", message } };
@@ -10,6 +11,7 @@ function streamErrorData(protocol, error, abort) {
10
11
  }
11
12
 
12
13
  export async function endStreamWithError(res, protocol, error, abort) {
14
+ markStreamFailure();
13
15
  if (res.destroyed || res.writableEnded) return;
14
16
  const data = streamErrorData(protocol, error, abort);
15
17
  await writeOrDrain(res, `event: error\ndata: ${JSON.stringify(data)}\n\n`).catch(() => false);
@@ -0,0 +1,177 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { currentRequestContext } from "./request-context.mjs";
3
+
4
+ const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact", "messages"]);
5
+ const TTFT_EDGES_MS = Object.freeze([
6
+ 100, 200, 300, 500, 700, 1_000, 1_400, 2_000, 2_800, 4_000,
7
+ 5_500, 8_000, 12_000, 16_000, 24_000, 36_000, 60_000, 120_000, 300_000,
8
+ ]);
9
+ const TPOT_EDGES_US = Object.freeze([
10
+ 500, 1_000, 2_000, 3_333, 5_000, 6_667, 8_333, 10_000, 12_500, 14_286,
11
+ 16_667, 20_000, 25_000, 33_333, 40_000, 50_000, 66_667, 100_000,
12
+ 150_000, 250_000, 500_000, 1_000_000, 2_500_000, 10_000_000,
13
+ ]);
14
+
15
+ const RESPONSES_OUTPUT_EVENT_TYPES = new Set([
16
+ "response.output_text.delta",
17
+ "response.function_call_arguments.delta",
18
+ "response.custom_tool_call_input.delta",
19
+ "response.refusal.delta",
20
+ "response.reasoning_text.delta",
21
+ "response.reasoning_summary_text.delta",
22
+ ]);
23
+
24
+ function nonEmptyString(value) {
25
+ return typeof value === "string" && value.length > 0;
26
+ }
27
+
28
+ export function isResponsesOutputEvent(event, eventType = event?.type) {
29
+ return RESPONSES_OUTPUT_EVENT_TYPES.has(eventType) && nonEmptyString(event?.delta);
30
+ }
31
+
32
+ export function isChatOutputDelta(delta) {
33
+ if (!delta || typeof delta !== "object") return false;
34
+ return nonEmptyString(delta.content)
35
+ || (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0)
36
+ || nonEmptyString(delta.refusal)
37
+ || nonEmptyString(delta.reasoning)
38
+ || nonEmptyString(delta.reasoning_content)
39
+ || nonEmptyString(delta.reasoning_text);
40
+ }
41
+
42
+ function createHistogram(edges) {
43
+ return {
44
+ edges,
45
+ counts: new Uint32Array(edges.length + 1),
46
+ samples: 0,
47
+ total: 0,
48
+ max: 0,
49
+ };
50
+ }
51
+
52
+ function observe(histogram, value) {
53
+ if (!Number.isFinite(value) || value < 0) return;
54
+ let index = 0;
55
+ while (index < histogram.edges.length && value > histogram.edges[index]) index += 1;
56
+ histogram.counts[index] += 1;
57
+ histogram.samples += 1;
58
+ histogram.total += value;
59
+ histogram.max = Math.max(histogram.max, value);
60
+ }
61
+
62
+ function histogramSnapshot(histogram, unit) {
63
+ let lower = 0;
64
+ const buckets = histogram.edges.map((upper, index) => {
65
+ const bucket = { lower, upper, count: histogram.counts[index] };
66
+ lower = upper;
67
+ return bucket;
68
+ });
69
+ buckets.push({ lower, upper: null, count: histogram.counts.at(-1) });
70
+ return {
71
+ unit,
72
+ samples: histogram.samples,
73
+ avg: histogram.samples > 0 ? Number((histogram.total / histogram.samples).toFixed(1)) : 0,
74
+ max: histogram.max,
75
+ buckets,
76
+ };
77
+ }
78
+
79
+ function createRoutePerformance() {
80
+ return {
81
+ ttft: createHistogram(TTFT_EDGES_MS),
82
+ tpot: createHistogram(TPOT_EDGES_US),
83
+ success_with_output: 0,
84
+ errors_with_output: 0,
85
+ zero_output_errors: 0,
86
+ neutral: 0,
87
+ };
88
+ }
89
+
90
+ function routeSnapshot(route) {
91
+ return {
92
+ success_with_output: route.success_with_output,
93
+ errors_with_output: route.errors_with_output,
94
+ zero_output_errors: route.zero_output_errors,
95
+ neutral: route.neutral,
96
+ ttft_ms: histogramSnapshot(route.ttft, "ms"),
97
+ tpot_us: histogramSnapshot(route.tpot, "us"),
98
+ };
99
+ }
100
+
101
+ export function createStreamPerformanceMetrics({ now = () => performance.now() } = {}) {
102
+ const routes = Object.fromEntries(PERFORMANCE_ROUTES.map((name) => [name, createRoutePerformance()]));
103
+
104
+ return {
105
+ begin(routeName) {
106
+ const route = routes[routeName];
107
+ if (!route) return null;
108
+ let upstreamStartedAt = null;
109
+ let firstOutputAt = null;
110
+ let outputTokens = null;
111
+ let failed = false;
112
+ let finished = false;
113
+
114
+ return {
115
+ upstreamStarted() {
116
+ if (upstreamStartedAt === null) upstreamStartedAt = now();
117
+ },
118
+ firstOutput() {
119
+ if (firstOutputAt === null) firstOutputAt = now();
120
+ },
121
+ setOutputTokens(value) {
122
+ const tokens = Number(value);
123
+ if (Number.isFinite(tokens) && tokens >= 0) outputTokens = tokens;
124
+ },
125
+ fail() {
126
+ failed = true;
127
+ },
128
+ finish({ failed: finishFailed = false } = {}) {
129
+ if (finished) return;
130
+ finished = true;
131
+ failed ||= finishFailed;
132
+ const finishedAt = now();
133
+ if (upstreamStartedAt === null) {
134
+ route.neutral += 1;
135
+ return;
136
+ }
137
+ if (firstOutputAt === null) {
138
+ if (failed) route.zero_output_errors += 1;
139
+ else route.neutral += 1;
140
+ return;
141
+ }
142
+ observe(route.ttft, Math.max(0, firstOutputAt - upstreamStartedAt));
143
+ if (outputTokens !== null && outputTokens >= 2) {
144
+ observe(route.tpot, Math.max(0, ((finishedAt - firstOutputAt) * 1_000) / (outputTokens - 1)));
145
+ }
146
+ if (failed) route.errors_with_output += 1;
147
+ else route.success_with_output += 1;
148
+ },
149
+ };
150
+ },
151
+ snapshot() {
152
+ return {
153
+ by_route: Object.fromEntries(PERFORMANCE_ROUTES.map((name) => [name, routeSnapshot(routes[name])])),
154
+ };
155
+ },
156
+ };
157
+ }
158
+
159
+ function tracker() {
160
+ return currentRequestContext()?.streamPerformance || null;
161
+ }
162
+
163
+ export function markUpstreamStarted() {
164
+ tracker()?.upstreamStarted();
165
+ }
166
+
167
+ export function markFirstOutput() {
168
+ tracker()?.firstOutput();
169
+ }
170
+
171
+ export function markOutputTokens(value) {
172
+ tracker()?.setOutputTokens(value);
173
+ }
174
+
175
+ export function markStreamFailure() {
176
+ tracker()?.fail();
177
+ }
@@ -0,0 +1,67 @@
1
+ const REQUEST_ID_HEADERS = [
2
+ "x-request-id",
3
+ "x-github-request-id",
4
+ "x-ms-request-id",
5
+ "request-id",
6
+ "x-correlation-id",
7
+ ];
8
+
9
+ const SAFE_EXACT_HEADERS = new Set([
10
+ "retry-after",
11
+ "ratelimit-limit",
12
+ "ratelimit-remaining",
13
+ "ratelimit-reset",
14
+ "ratelimit-policy",
15
+ "openai-model",
16
+ "openai-processing-ms",
17
+ "traceparent",
18
+ ]);
19
+
20
+ const SAFE_HEADER_PREFIXES = [
21
+ "x-ratelimit-",
22
+ "x-ms-ratelimit-",
23
+ ];
24
+
25
+ function headerValue(headers, name) {
26
+ const value = headers?.get?.(name);
27
+ return typeof value === "string" && value.trim() ? value : null;
28
+ }
29
+
30
+ function canonicalHeaderName(name) {
31
+ return String(name).split("-").map((part) => (
32
+ part ? `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}` : part
33
+ )).join("-");
34
+ }
35
+
36
+ function isSafeMetadataHeader(name) {
37
+ return SAFE_EXACT_HEADERS.has(name)
38
+ || SAFE_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix));
39
+ }
40
+
41
+ export function safeUpstreamResponseHeaders(headers, {
42
+ contentType,
43
+ defaultContentType,
44
+ } = {}) {
45
+ const out = {};
46
+ const resolvedContentType = contentType
47
+ ?? headerValue(headers, "content-type")
48
+ ?? defaultContentType;
49
+ if (resolvedContentType) out["Content-Type"] = resolvedContentType;
50
+
51
+ if (headers && typeof headers[Symbol.iterator] === "function") {
52
+ for (const [rawName, value] of headers) {
53
+ const name = String(rawName).toLowerCase();
54
+ if (!isSafeMetadataHeader(name)) continue;
55
+ out[canonicalHeaderName(name)] = value;
56
+ }
57
+ }
58
+
59
+ for (const name of REQUEST_ID_HEADERS) {
60
+ const value = headerValue(headers, name);
61
+ if (value) {
62
+ out["X-Upstream-Request-Id"] = value;
63
+ break;
64
+ }
65
+ }
66
+ return out;
67
+ }