codex-copilot-dx 0.4.43 → 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.
@@ -6,33 +6,82 @@ import {
6
6
  writeOrDrain,
7
7
  } from "./http-transport.mjs";
8
8
  import { openCopilotResponse, rememberResponseHistory } from "./responses-request.mjs";
9
+ import {
10
+ compactionInputWithoutTrigger,
11
+ parseResponsesCompactionResult,
12
+ } from "./responses-compaction.mjs";
9
13
  import { loadRuntimeConfig } from "./runtime-config.mjs";
10
14
  import { endStreamWithError } from "./stream-errors.mjs";
15
+ import {
16
+ incompleteUpstreamStream,
17
+ invalidUpstreamStream,
18
+ requireUpstreamEventStream,
19
+ ToolArgumentDeltaGuard,
20
+ } from "./stream-contract.mjs";
21
+ import {
22
+ isResponsesOutputEvent,
23
+ markFirstOutput,
24
+ markOutputTokens,
25
+ markStreamFailure,
26
+ } from "./stream-performance.mjs";
27
+ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
11
28
  import { recordResponsesUsage } from "./usage.mjs";
12
29
 
13
30
  const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
31
+ const RESPONSES_TERMINAL_EVENT_TYPES = new Set([
32
+ "response.completed",
33
+ "response.incomplete",
34
+ "response.failed",
35
+ "error",
36
+ ]);
14
37
 
15
- function storeCompletedResponseFromSse(reqContext, data) {
16
- if (!data || data === "[DONE]") return;
38
+ function inspectResponseSseEvent(state, eventName, data) {
39
+ if (!data || state.sawTerminal) return;
40
+ if (data === "[DONE]") {
41
+ throw invalidUpstreamStream(
42
+ "Upstream sent [DONE] before a terminal Responses event",
43
+ "upstream_stream_terminal_missing",
44
+ );
45
+ }
46
+ let event;
17
47
  try {
18
- const event = JSON.parse(data);
19
- const response = event.response;
20
- if (response?.id && response.status === "completed") {
21
- rememberResponseHistory(reqContext, response);
22
- recordResponsesUsage({
23
- surface: reqContext.surface,
24
- mode: "stream",
25
- model: reqContext.body?.model,
26
- response,
27
- event,
28
- });
29
- }
48
+ event = JSON.parse(data);
30
49
  } catch {
31
- // Ignore non-JSON stream fragments.
50
+ throw invalidUpstreamStream(
51
+ "Upstream Responses SSE event contained invalid JSON",
52
+ "upstream_stream_json_invalid",
53
+ );
54
+ }
55
+ const eventType = event.type || eventName;
56
+ const outputTokens = event.response?.usage?.output_tokens ?? event.usage?.output_tokens;
57
+ if (outputTokens !== undefined) markOutputTokens(outputTokens);
58
+ if (!state.sawOutput && isResponsesOutputEvent(event, eventType)) {
59
+ state.sawOutput = true;
60
+ markFirstOutput();
61
+ }
62
+ if (["response.function_call_arguments.delta", "response.custom_tool_call_input.delta"].includes(eventType)) {
63
+ state.toolArgumentGuard.observe(event.output_index ?? event.item_id ?? event.call_id, event.delta);
32
64
  }
65
+ if (!RESPONSES_TERMINAL_EVENT_TYPES.has(eventType)) return;
66
+ const expectedStatus = {
67
+ "response.completed": "completed",
68
+ "response.incomplete": "incomplete",
69
+ "response.failed": "failed",
70
+ }[eventType];
71
+ if (expectedStatus && (!event.response?.id || event.response.status !== expectedStatus)) {
72
+ throw invalidUpstreamStream(
73
+ `Upstream ${eventType} event contained an invalid response resource`,
74
+ "upstream_stream_terminal_invalid",
75
+ );
76
+ }
77
+ state.sawTerminal = true;
78
+ if (eventType === "response.failed" || eventType === "error") markStreamFailure();
79
+ if (eventType !== "response.completed") return;
80
+ const response = event.response;
81
+ state.completed = { response, event };
33
82
  }
34
83
 
35
- function readSseEvents(buffer, onData) {
84
+ function readSseEvents(buffer, onEvent) {
36
85
  while (true) {
37
86
  const match = buffer.match(/\r?\n\r?\n/);
38
87
  if (!match) return buffer;
@@ -41,15 +90,31 @@ function readSseEvents(buffer, onData) {
41
90
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
42
91
  }
43
92
  buffer = buffer.slice(match.index + match[0].length);
44
- const data = chunk
45
- .split(/\r?\n/)
93
+ const lines = chunk.split(/\r?\n/);
94
+ const eventName = lines
95
+ .find((line) => line.startsWith("event:"))
96
+ ?.slice(6)
97
+ .trim();
98
+ const data = lines
46
99
  .filter((line) => line.startsWith("data:"))
47
100
  .map((line) => line.slice(5).trimStart())
48
101
  .join("\n");
49
- if (data) onData(data);
102
+ if (data) onEvent(eventName, data);
50
103
  }
51
104
  }
52
105
 
106
+ function storeCompletedResponse(reqContext, completed) {
107
+ if (!completed) return;
108
+ rememberResponseHistory(reqContext, completed.response);
109
+ recordResponsesUsage({
110
+ surface: reqContext.surface,
111
+ mode: "stream",
112
+ model: reqContext.body?.model,
113
+ response: completed.response,
114
+ event: completed.event,
115
+ });
116
+ }
117
+
53
118
  export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
54
119
  let opened;
55
120
  try {
@@ -65,15 +130,22 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
65
130
  }
66
131
 
67
132
  if (reqContext.body.stream) {
133
+ await requireUpstreamEventStream(resp);
68
134
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
69
135
  res.writeHead(resp.status, {
70
- "Content-Type": resp.headers.get("content-type") || "text/event-stream",
136
+ ...safeUpstreamResponseHeaders(resp.headers, { defaultContentType: "text/event-stream" }),
71
137
  "Cache-Control": "no-cache",
72
138
  Connection: "keep-alive",
73
139
  });
74
140
  const reader = resp.body.getReader();
75
141
  const decoder = new TextDecoder();
76
142
  let buffer = "";
143
+ const streamState = {
144
+ completed: null,
145
+ sawTerminal: false,
146
+ sawOutput: false,
147
+ toolArgumentGuard: new ToolArgumentDeltaGuard(),
148
+ };
77
149
  try {
78
150
  while (true) {
79
151
  const { done, value } = await reader.read();
@@ -82,17 +154,24 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
82
154
  if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
83
155
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
84
156
  }
85
- readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
157
+ readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
158
+ if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
159
+ storeCompletedResponse(reqContext, streamState.completed);
86
160
  res.end();
87
161
  return;
88
162
  }
89
163
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
90
164
  buffer += decoder.decode(value, { stream: true });
91
- buffer = readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
165
+ buffer = readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
92
166
  if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
93
167
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
94
168
  }
95
169
  if (!await writeOrDrain(res, value)) return;
170
+ if (streamState.sawTerminal) {
171
+ storeCompletedResponse(reqContext, streamState.completed);
172
+ res.end();
173
+ return;
174
+ }
96
175
  }
97
176
  } catch (e) {
98
177
  logRequestFailure("Responses", e, options.abort);
@@ -103,7 +182,28 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
103
182
  }
104
183
  } else {
105
184
  const data = await resp.text();
106
- res.writeHead(resp.status, { "Content-Type": "application/json" });
185
+ if (resp.ok && reqContext.surface === "responses_compact") {
186
+ const response = parseResponsesCompactionResult(
187
+ compactionInputWithoutTrigger(reqContext.body.input),
188
+ data,
189
+ );
190
+ rememberResponseHistory(reqContext, response);
191
+ recordResponsesUsage({
192
+ surface: reqContext.surface,
193
+ mode: "json",
194
+ model: reqContext.body?.model,
195
+ response,
196
+ event: response,
197
+ });
198
+ res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
199
+ contentType: "application/json",
200
+ }));
201
+ res.end(JSON.stringify(response));
202
+ return;
203
+ }
204
+ res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
205
+ contentType: "application/json",
206
+ }));
107
207
  res.end(data);
108
208
  if (resp.ok) {
109
209
  try {
@@ -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
+ }