codex-copilot-dx 0.4.43 → 0.5.0

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.
@@ -0,0 +1,113 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { httpError } from "./http-transport.mjs";
3
+
4
+ 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
+
9
+ function compactionError(message) {
10
+ const error = httpError(message, 502);
11
+ error.code = "ccdx_invalid_compaction_response";
12
+ error.jsonBody = {
13
+ error: {
14
+ message,
15
+ type: "upstream_error",
16
+ code: error.code,
17
+ },
18
+ };
19
+ return error;
20
+ }
21
+
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
+ export function prepareResponsesCompactionRequest(reqContext) {
64
+ const input = Array.isArray(reqContext?.body?.input) ? reqContext.body.input : [];
65
+ const compactionInputItems = compactionInputWithoutTrigger(input);
66
+ reqContext.body.input = [...compactionInputItems, { type: COMPACTION_TRIGGER_TYPE }];
67
+ reqContext.body.stream = false;
68
+ return reqContext;
69
+ }
70
+
71
+ export function compactionInputWithoutTrigger(input) {
72
+ return Array.isArray(input)
73
+ ? input.filter((item) => item?.type !== COMPACTION_TRIGGER_TYPE)
74
+ : [];
75
+ }
76
+
77
+ export function createResponsesCompactionResult(inputItems, generated) {
78
+ if (!generated || typeof generated !== "object" || !generated.id || !Array.isArray(generated.output)) {
79
+ throw compactionError("Copilot compaction returned an invalid response envelope");
80
+ }
81
+ if (Object.prototype.hasOwnProperty.call(generated, "status") && generated.status !== "completed") {
82
+ throw compactionError(`Copilot compaction returned status ${generated.status}`);
83
+ }
84
+
85
+ const compactionItems = generated.output.filter((item) => item?.type === "compaction");
86
+ if (!compactionItems.length) {
87
+ throw compactionError("Copilot compaction returned no compaction output item");
88
+ }
89
+ if (compactionItems.some((item) => (
90
+ typeof item.encrypted_content !== "string" || item.encrypted_content.length === 0
91
+ ))) {
92
+ throw compactionError("Copilot compaction returned an invalid compaction output item");
93
+ }
94
+
95
+ return {
96
+ ...generated,
97
+ object: "response.compaction",
98
+ output: [
99
+ ...retainedMessages(Array.isArray(inputItems) ? inputItems : []),
100
+ ...compactionItems,
101
+ ],
102
+ };
103
+ }
104
+
105
+ export function parseResponsesCompactionResult(inputItems, text) {
106
+ let generated;
107
+ try {
108
+ generated = JSON.parse(text);
109
+ } catch {
110
+ throw compactionError("Copilot compaction returned invalid JSON");
111
+ }
112
+ return createResponsesCompactionResult(inputItems, generated);
113
+ }
@@ -15,10 +15,12 @@ import {
15
15
  } from "./models.mjs";
16
16
  import { chatToResponses, forwardToChat, responsesBodyUsesCustomTools } from "./responses-bridge.mjs";
17
17
  import { prepareResponsesChatPayload } from "./responses-chat-payload.mjs";
18
+ import { prepareResponsesCompactionRequest } from "./responses-compaction.mjs";
18
19
  import { proxyCopilotResponses } from "./responses-proxy.mjs";
19
20
  import { prepareResponsesRequest, rememberResponseHistory } from "./responses-request.mjs";
20
21
  import { status } from "./status.mjs";
21
22
  import { endStreamWithError } from "./stream-errors.mjs";
23
+ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
22
24
  import { recordResponsesUsage } from "./usage.mjs";
23
25
 
24
26
  const RESPONSES_ONLY_FALLBACK = new Set([
@@ -113,20 +115,29 @@ export function createResponsesHandler(options) {
113
115
  });
114
116
  const { chatReq, bodyText } = chatPayload;
115
117
  if (parsed.stream) {
118
+ let streamResponseHeaders = null;
116
119
  await forwardToChat(chatReq, async (event, data) => {
117
- if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
120
+ if (!res.headersSent) res.writeHead(200, {
121
+ ...streamResponseHeaders,
122
+ "Cache-Control": "no-cache",
123
+ Connection: "keep-alive",
124
+ });
118
125
  await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
119
126
  if (event === "response.completed") {
120
127
  rememberResponseHistory(prepared, data.response);
121
128
  recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
122
129
  }
123
- }, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg) => {
130
+ }, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
124
131
  if (!res.headersSent) {
125
- res.writeHead(statusCode || 500, { "Content-Type": "text/plain; charset=utf-8" });
126
- res.end(errMsg);
132
+ if (upstreamResponse) {
133
+ sendUpstreamError(res, upstreamResponse, errMsg);
134
+ return;
135
+ }
136
+ const responseError = error || Object.assign(new Error(errMsg), { statusCode });
137
+ sendJsonError(res, responseError, statusCode || 500);
127
138
  return;
128
139
  }
129
- await endStreamWithError(res, "responses", new Error(errMsg), abort);
140
+ await endStreamWithError(res, "responses", error || new Error(errMsg), abort);
130
141
  }, {
131
142
  signal: abort.signal,
132
143
  abort,
@@ -134,6 +145,11 @@ export function createResponsesHandler(options) {
134
145
  chatCompletionsFn,
135
146
  releaseRequest,
136
147
  bodyText,
148
+ onUpstreamResponse: (upstreamResponse) => {
149
+ streamResponseHeaders = safeUpstreamResponseHeaders(upstreamResponse.headers, {
150
+ contentType: "text/event-stream",
151
+ });
152
+ },
137
153
  });
138
154
  } else {
139
155
  try {
@@ -147,7 +163,9 @@ export function createResponsesHandler(options) {
147
163
  const response = chatToResponses(JSON.parse(data), model);
148
164
  rememberResponseHistory(prepared, response);
149
165
  recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
150
- res.writeHead(200, { "Content-Type": "application/json" });
166
+ res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
167
+ contentType: "application/json",
168
+ }));
151
169
  res.end(JSON.stringify(response));
152
170
  } catch (error) {
153
171
  logRequestFailure("Responses", error, abort);
@@ -170,8 +188,6 @@ export function createResponsesCompactHandler(options) {
170
188
  acquireRequest,
171
189
  openAIModelEnv,
172
190
  responsesCompactFn,
173
- streamHandshakeTimeoutMs,
174
- streamIdleTimeoutMs,
175
191
  upstreamTimeoutMs,
176
192
  } = options;
177
193
 
@@ -181,22 +197,20 @@ export function createResponsesCompactHandler(options) {
181
197
  try {
182
198
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
183
199
  const parsed = await readJsonBody(req);
184
- abort.setTimeout(
185
- parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
186
- parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
200
+ abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
201
+ const prepared = prepareResponsesCompactionRequest(
202
+ prepareResponsesRequest(parsed, { mutate: true }),
187
203
  );
188
- const prepared = prepareResponsesRequest(parsed, { mutate: true });
189
204
  prepared.surface = "responses_compact";
190
205
  const model = parsed.model || "unknown";
191
206
  const { requestedModel, upstreamModel } = resolveOpenAIModel(model, openAIModelEnv);
192
207
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
193
208
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
194
- console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
209
+ console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
195
210
  await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
196
211
  signal: abort.signal,
197
212
  abort,
198
213
  releaseRequest,
199
- streamIdleTimeoutMs,
200
214
  });
201
215
  } catch (error) {
202
216
  logRequestFailure("Responses compact", error, abort);
@@ -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 {
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { localPackageVersion } from "./version.mjs";
3
3
 
4
4
  export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
5
+ export const ADAPTER_STATUS_PATH = "/_ccdx/status";
5
6
  export const ADAPTER_PROTOCOL_VERSION = 2;
6
7
  export const ADAPTER_VERSION = localPackageVersion();
7
8
  const ADAPTER_INSTANCE_ID = randomUUID();
@@ -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);