@dianshuv/copilot-api 0.14.0 → 0.15.1

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 (3) hide show
  1. package/README.md +1 -4
  2. package/dist/main.mjs +217 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Copilot API Proxy (Fork)
2
2
 
3
3
  > [!NOTE]
4
- > This is a fork of [@hsupu/copilot-api](https://www.npmjs.com/package/@hsupu/copilot-api), which itself is a fork of [ericc-ch/copilot-api](https://github.com/ericc-ch/copilot-api), with additional improvements and bug fixes.
4
+ > This is a fork of [@hsupu/copilot-api](https://www.npmjs.com/package/@hsupu/copilot-api), which itself is a fork of `ericc-ch/copilot-api`, with additional improvements and bug fixes.
5
5
 
6
6
  > [!WARNING]
7
7
  > This is a reverse-engineered proxy of GitHub Copilot API. It is not supported by GitHub, and may break unexpectedly. Use at your own risk.
@@ -275,6 +275,3 @@ Create `.claude/settings.json` in your project:
275
275
  }
276
276
  ```
277
277
 
278
- ## Upstream Project
279
-
280
- For the original project documentation, features, and updates, see: [ericc-ch/copilot-api](https://github.com/ericc-ch/copilot-api)
package/dist/main.mjs CHANGED
@@ -1086,7 +1086,7 @@ const logout = defineCommand({
1086
1086
 
1087
1087
  //#endregion
1088
1088
  //#region package.json
1089
- var version = "0.14.0";
1089
+ var version = "0.15.1";
1090
1090
 
1091
1091
  //#endregion
1092
1092
  //#region src/lib/event-loop-lag.ts
@@ -3215,6 +3215,43 @@ const awaitApproval = async () => {
3215
3215
  if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
3216
3216
  };
3217
3217
 
3218
+ //#endregion
3219
+ //#region src/lib/client-abort.ts
3220
+ /**
3221
+ * Build a per-request AbortController that fires when the downstream client
3222
+ * goes away, for forwarding into the upstream Copilot `fetch`.
3223
+ *
3224
+ * Why a controller instead of passing `c.req.raw.signal` straight through:
3225
+ * the client-disconnect signal is delivered differently per runtime. Verified
3226
+ * on the runtime the published package runs under (Node/srvx): BOTH
3227
+ * `c.req.raw.signal` and Hono's `stream.onAbort` fire on disconnect. Rather
3228
+ * than trust a single source (a wrong bet here silently leaks), callers wire
3229
+ * `c.req.raw.signal` (here) AND `stream.onAbort` (in the streaming branch) into
3230
+ * the one controller, then pass `controller.signal` to the upstream fetch.
3231
+ *
3232
+ * Without this, an abandoned request keeps draining the upstream response to
3233
+ * completion — holding one of the account's scarce concurrent-request slots
3234
+ * until the proxy process dies (which is why restarting the proxy "fixes" a
3235
+ * pile-up of slow requests).
3236
+ */
3237
+ function clientAbortController(c) {
3238
+ const controller = new AbortController();
3239
+ const clientSignal = c.req.raw.signal;
3240
+ if (clientSignal.aborted) controller.abort();
3241
+ else clientSignal.addEventListener("abort", () => controller.abort(), { once: true });
3242
+ return controller;
3243
+ }
3244
+ /**
3245
+ * True when an error originates from an AbortSignal firing (client disconnect)
3246
+ * rather than a genuine upstream failure, so callers can treat a cancelled
3247
+ * request as a clean stop instead of recording a spurious error. Matches both
3248
+ * `Error` and `DOMException` (undici's fetch rejects with the latter, which is
3249
+ * not always an `instanceof Error`), keying only on the `name`.
3250
+ */
3251
+ function isAbortError(error) {
3252
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
3253
+ }
3254
+
3218
3255
  //#endregion
3219
3256
  //#region src/lib/message-sanitizer.ts
3220
3257
  const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
@@ -3911,7 +3948,8 @@ const createChatCompletions = async (payload, options) => {
3911
3948
  const response = await copilotFetch("/chat/completions", {
3912
3949
  method: "POST",
3913
3950
  headers,
3914
- body: JSON.stringify(wire)
3951
+ body: JSON.stringify(wire),
3952
+ signal: options?.signal
3915
3953
  });
3916
3954
  if (!response.ok) {
3917
3955
  consola.error("Failed to create chat completions", response);
@@ -4346,13 +4384,18 @@ async function handleCompletion$1(c) {
4346
4384
  */
4347
4385
  async function executeRequest(opts) {
4348
4386
  const { c, payload, selectedModel, ctx } = opts;
4387
+ const abort = clientAbortController(c);
4349
4388
  try {
4350
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, { resolvedModel: selectedModel }));
4389
+ const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload, {
4390
+ resolvedModel: selectedModel,
4391
+ signal: abort.signal
4392
+ }));
4351
4393
  ctx.queueWaitMs = queueWaitMs;
4352
4394
  if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
4353
4395
  consola.debug("Streaming response");
4354
4396
  updateTrackerStatus(ctx.trackingId, "streaming");
4355
4397
  return streamSSE(c, async (stream) => {
4398
+ stream.onAbort(() => abort.abort());
4356
4399
  await handleStreamingResponse$1({
4357
4400
  stream,
4358
4401
  response,
@@ -4361,6 +4404,11 @@ async function executeRequest(opts) {
4361
4404
  });
4362
4405
  });
4363
4406
  } catch (error) {
4407
+ if (isAbortError(error)) {
4408
+ consola.debug("[ChatCompletions] client disconnected before response; upstream aborted");
4409
+ failTracking(ctx.trackingId, "client disconnected");
4410
+ return new Response(null, { status: 499 });
4411
+ }
4364
4412
  if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(payload, selectedModel);
4365
4413
  recordErrorResponse(ctx, payload.model, error, "chat_completions", payload.stream ?? false);
4366
4414
  throw error;
@@ -4499,6 +4547,11 @@ async function handleStreamingResponse$1(opts) {
4499
4547
  toolCount: payload.tools?.length ?? 0
4500
4548
  }, ctx.timings);
4501
4549
  } catch (error) {
4550
+ if (isAbortError(error)) {
4551
+ consola.debug("[ChatCompletions] client disconnected mid-stream; upstream aborted");
4552
+ failTracking(ctx.trackingId, "client disconnected");
4553
+ return;
4554
+ }
4502
4555
  recordStreamError({
4503
4556
  acc,
4504
4557
  fallbackModel: payload.model,
@@ -6837,7 +6890,8 @@ async function createAnthropicMessages(payload, options) {
6837
6890
  const response = await copilotFetch("/v1/messages", {
6838
6891
  method: "POST",
6839
6892
  headers,
6840
- body: JSON.stringify(filteredPayload)
6893
+ body: JSON.stringify(filteredPayload),
6894
+ signal: options?.signal
6841
6895
  });
6842
6896
  if (!response.ok) {
6843
6897
  consola.debug("Request failed:", {
@@ -6944,6 +6998,74 @@ function supportsDirectAnthropicApi(modelId) {
6944
6998
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
6945
6999
  }
6946
7000
 
7001
+ //#endregion
7002
+ //#region src/routes/messages/cache-control-injector.ts
7003
+ function blockHasCacheControl(block) {
7004
+ if (typeof block !== "object" || block === null) return false;
7005
+ if (!("cache_control" in block)) return false;
7006
+ const cc = block.cache_control;
7007
+ return cc !== void 0 && cc !== null;
7008
+ }
7009
+ function toolsHaveCacheControl(tools) {
7010
+ if (!Array.isArray(tools)) return false;
7011
+ return tools.some((t) => blockHasCacheControl(t));
7012
+ }
7013
+ function toolResultInnerHasCacheControl(block) {
7014
+ if (!Array.isArray(block.content)) return false;
7015
+ return block.content.some((inner) => blockHasCacheControl(inner));
7016
+ }
7017
+ function messagesHaveCacheControl(messages) {
7018
+ if (!Array.isArray(messages)) return false;
7019
+ for (const message of messages) {
7020
+ if (!Array.isArray(message.content)) continue;
7021
+ for (const block of message.content) {
7022
+ if (blockHasCacheControl(block)) return true;
7023
+ if (block.type === "tool_result" && toolResultInnerHasCacheControl(block)) return true;
7024
+ }
7025
+ }
7026
+ return false;
7027
+ }
7028
+ function systemHasCacheControl(system) {
7029
+ if (!Array.isArray(system)) return false;
7030
+ return system.some((b) => blockHasCacheControl(b));
7031
+ }
7032
+ function hasAnyCacheControl(payload) {
7033
+ return systemHasCacheControl(payload.system) || toolsHaveCacheControl(payload.tools) || messagesHaveCacheControl(payload.messages);
7034
+ }
7035
+ function injectSystemCacheControl(payload) {
7036
+ if (hasAnyCacheControl(payload)) return;
7037
+ if (payload.system === void 0 || payload.system === null) return;
7038
+ if (typeof payload.system === "string") {
7039
+ if (payload.system.length === 0) return;
7040
+ payload.system = [{
7041
+ type: "text",
7042
+ text: payload.system,
7043
+ cache_control: { type: "ephemeral" }
7044
+ }];
7045
+ return;
7046
+ }
7047
+ if (payload.system.length === 0) return;
7048
+ const tail = payload.system.at(-1);
7049
+ if (!tail) return;
7050
+ tail.cache_control = { type: "ephemeral" };
7051
+ }
7052
+
7053
+ //#endregion
7054
+ //#region src/routes/messages/date-normalizer.ts
7055
+ const dateTrackingRegex = /(# currentDate\r?\n)Today['’ʼʹ]s date is (\d{4})[/-](\d{2})[/-](\d{2})\.(\r?\n|$)/g;
7056
+ function normalizeClaudeCodeDate(text) {
7057
+ return text.replaceAll(dateTrackingRegex, "$1Today's date is $2-$3-$4.$5");
7058
+ }
7059
+ function normalizeSystemPromptDate(payload) {
7060
+ if (typeof payload.system === "string") {
7061
+ payload.system = normalizeClaudeCodeDate(payload.system);
7062
+ return;
7063
+ }
7064
+ if (Array.isArray(payload.system)) {
7065
+ for (const block of payload.system) if (typeof block.text === "string") block.text = normalizeClaudeCodeDate(block.text);
7066
+ }
7067
+ }
7068
+
6947
7069
  //#endregion
6948
7070
  //#region src/lib/stream-keepalive.ts
6949
7071
  /** SSE comment line used as a keepalive heartbeat. */
@@ -8227,13 +8349,15 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8227
8349
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
8228
8350
  if (state.manualApprove) await awaitApproval();
8229
8351
  const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
8352
+ const abort = clientAbortController(c);
8230
8353
  const isStreaming = anthropicPayload.stream === true;
8231
8354
  try {
8232
8355
  const settled = settle(executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8233
8356
  initiator: initiatorOverride,
8234
8357
  injectContext1mBeta: needsContext1mBeta,
8235
8358
  errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
8236
- clientAnthropicBetaHeader
8359
+ clientAnthropicBetaHeader,
8360
+ signal: abort.signal
8237
8361
  })));
8238
8362
  const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8239
8363
  if (raced.kind === "error") throw raced.error;
@@ -8244,6 +8368,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8244
8368
  consola.debug("Streaming response from Copilot (direct Anthropic)");
8245
8369
  updateTrackerStatus(ctx.trackingId, "streaming");
8246
8370
  return streamSSE(c, async (stream) => {
8371
+ stream.onAbort(() => abort.abort());
8247
8372
  await handleDirectAnthropicStreamingResponse({
8248
8373
  stream,
8249
8374
  response,
@@ -8257,6 +8382,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8257
8382
  consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
8258
8383
  updateTrackerStatus(ctx.trackingId, "streaming");
8259
8384
  return streamSSE(c, async (stream) => {
8385
+ stream.onAbort(() => abort.abort());
8260
8386
  await runStreamWithKeepalive({
8261
8387
  stream,
8262
8388
  settled,
@@ -8271,6 +8397,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8271
8397
  });
8272
8398
  },
8273
8399
  onError: async (error) => {
8400
+ if (isAbortError(error)) {
8401
+ consola.debug("[Anthropic] client disconnected during keepalive; upstream aborted");
8402
+ failTracking(ctx.trackingId, "client disconnected");
8403
+ return;
8404
+ }
8274
8405
  recordStreamError({
8275
8406
  acc: createAnthropicStreamAccumulator(),
8276
8407
  fallbackModel: anthropicPayload.model,
@@ -8288,6 +8419,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8288
8419
  });
8289
8420
  });
8290
8421
  } catch (error) {
8422
+ if (isAbortError(error)) {
8423
+ consola.debug("[Anthropic] client disconnected before response; upstream aborted");
8424
+ failTracking(ctx.trackingId, "client disconnected");
8425
+ return new Response(null, { status: 499 });
8426
+ }
8291
8427
  if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
8292
8428
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
8293
8429
  throw error;
@@ -8442,6 +8578,11 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8442
8578
  toolCount: anthropicPayload.tools?.length ?? 0
8443
8579
  }, ctx.timings);
8444
8580
  } catch (error) {
8581
+ if (isAbortError(error)) {
8582
+ consola.debug("[Anthropic] client disconnected mid-stream; upstream aborted");
8583
+ failTracking(ctx.trackingId, "client disconnected");
8584
+ return;
8585
+ }
8445
8586
  consola.error("Direct Anthropic stream error:", formatError(error));
8446
8587
  recordStreamError({
8447
8588
  acc,
@@ -8531,13 +8672,15 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8531
8672
  if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8532
8673
  else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8533
8674
  else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8675
+ const abort = clientAbortController(c);
8534
8676
  const isStreaming = anthropicPayload.stream === true;
8535
8677
  try {
8536
8678
  const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8537
8679
  initiator: initiatorOverride,
8538
8680
  resolvedModel: selectedModel,
8539
8681
  anthropicBeta,
8540
- errorModelIdOverride
8682
+ errorModelIdOverride,
8683
+ signal: abort.signal
8541
8684
  })));
8542
8685
  const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8543
8686
  if (raced.kind === "error") throw raced.error;
@@ -8554,6 +8697,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8554
8697
  consola.debug("Streaming response from Copilot");
8555
8698
  updateTrackerStatus(ctx.trackingId, "streaming");
8556
8699
  return streamSSE(c, async (stream) => {
8700
+ stream.onAbort(() => abort.abort());
8557
8701
  await handleStreamingResponse({
8558
8702
  stream,
8559
8703
  response,
@@ -8566,6 +8710,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8566
8710
  consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
8567
8711
  updateTrackerStatus(ctx.trackingId, "streaming");
8568
8712
  return streamSSE(c, async (stream) => {
8713
+ stream.onAbort(() => abort.abort());
8569
8714
  await runStreamWithKeepalive({
8570
8715
  stream,
8571
8716
  settled,
@@ -8582,6 +8727,11 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8582
8727
  });
8583
8728
  },
8584
8729
  onError: async (error) => {
8730
+ if (isAbortError(error)) {
8731
+ consola.debug("[Translated] client disconnected during keepalive; upstream aborted");
8732
+ failTracking(ctx.trackingId, "client disconnected");
8733
+ return;
8734
+ }
8585
8735
  recordStreamError({
8586
8736
  acc: createAnthropicStreamAccumulator(),
8587
8737
  fallbackModel: anthropicPayload.model,
@@ -8599,6 +8749,11 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8599
8749
  });
8600
8750
  });
8601
8751
  } catch (error) {
8752
+ if (isAbortError(error)) {
8753
+ consola.debug("[Translated] client disconnected before response; upstream aborted");
8754
+ failTracking(ctx.trackingId, "client disconnected");
8755
+ return new Response(null, { status: 499 });
8756
+ }
8602
8757
  if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
8603
8758
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
8604
8759
  throw error;
@@ -8687,6 +8842,11 @@ async function handleStreamingResponse(opts) {
8687
8842
  toolCount: anthropicPayload.tools?.length ?? 0
8688
8843
  }, ctx.timings);
8689
8844
  } catch (error) {
8845
+ if (isAbortError(error)) {
8846
+ consola.debug("[Translated] client disconnected mid-stream; upstream aborted");
8847
+ failTracking(ctx.trackingId, "client disconnected");
8848
+ return;
8849
+ }
8690
8850
  consola.error("Stream error:", formatError(error));
8691
8851
  recordStreamError({
8692
8852
  acc,
@@ -8805,7 +8965,11 @@ async function handleCompletion(c) {
8805
8965
  const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
8806
8966
  const initiatorOverride = subagentMarker ? "agent" : void 0;
8807
8967
  if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
8808
- if (supportsDirectAnthropicApi(sanitizedPayload.model)) return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8968
+ if (supportsDirectAnthropicApi(sanitizedPayload.model)) {
8969
+ normalizeSystemPromptDate(sanitizedPayload);
8970
+ injectSystemCacheControl(sanitizedPayload);
8971
+ return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8972
+ }
8809
8973
  return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
8810
8974
  }
8811
8975
  /**
@@ -8938,9 +9102,34 @@ modelRoutes.get("/", async (c) => {
8938
9102
  }
8939
9103
  });
8940
9104
 
9105
+ //#endregion
9106
+ //#region src/lib/prompt-cache-key.ts
9107
+ const CLIENT_NAME_PATTERNS = [
9108
+ [/^claude[-_]?(?:code|cli)$/i, "claude-code"],
9109
+ [/^codex$/i, "codex"],
9110
+ [/^cursor$/i, "cursor"],
9111
+ [/^aider$/i, "aider"],
9112
+ [/^copilot$/i, "copilot"]
9113
+ ];
9114
+ const UA_MAIN_TOKEN = /^([\w.-]+)/;
9115
+ function extractClientName(userAgent) {
9116
+ if (!userAgent) return "unknown";
9117
+ const mainToken = UA_MAIN_TOKEN.exec(userAgent)?.[1]?.toLowerCase();
9118
+ if (!mainToken) return "unknown";
9119
+ for (const [pattern, name] of CLIENT_NAME_PATTERNS) if (pattern.test(mainToken)) return name;
9120
+ return mainToken.slice(0, 32);
9121
+ }
9122
+ function buildPromptCacheKey(clientName) {
9123
+ return `copilot-api:${clientName}`;
9124
+ }
9125
+ function injectPromptCacheKey(payload, clientName) {
9126
+ if (payload.prompt_cache_key !== void 0 && payload.prompt_cache_key !== null && payload.prompt_cache_key !== "") return;
9127
+ payload.prompt_cache_key = buildPromptCacheKey(clientName);
9128
+ }
9129
+
8941
9130
  //#endregion
8942
9131
  //#region src/services/copilot/create-responses.ts
8943
- const createResponses = async (payload, { vision, initiator, resolvedModel }) => {
9132
+ const createResponses = async (payload, { vision, initiator, resolvedModel, signal }) => {
8944
9133
  if (!state.copilotToken) throw new Error("Copilot token not found");
8945
9134
  const modelSupportsVision = resolvedModel?.capabilities?.supports?.vision !== false;
8946
9135
  const headers = {
@@ -8954,7 +9143,8 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
8954
9143
  const response = await copilotFetch("/responses", {
8955
9144
  method: "POST",
8956
9145
  headers,
8957
- body: JSON.stringify(payload)
9146
+ body: JSON.stringify(payload),
9147
+ signal
8958
9148
  });
8959
9149
  if (!response.ok) {
8960
9150
  consola.error("Failed to create responses", response);
@@ -9195,14 +9385,17 @@ const TERMINAL_EVENTS = new Set([
9195
9385
  "error"
9196
9386
  ]);
9197
9387
  const handleResponses = async (c) => {
9388
+ const rawPayload = await c.req.json();
9389
+ const clientName = extractClientName(c.req.header("user-agent"));
9198
9390
  const { ctx, payload } = createEntryContext({
9199
9391
  c,
9200
- rawPayload: await c.req.json(),
9392
+ rawPayload,
9201
9393
  endpoint: "openai",
9202
9394
  normalizePayload: (p) => {
9203
9395
  const np = state.normalizeResponsesCallIds ? normalizeCallIds(p) : p;
9204
9396
  useFunctionApplyPatch(np);
9205
9397
  filterUnsupportedBuiltins(np);
9398
+ injectPromptCacheKey(np, clientName);
9206
9399
  return np;
9207
9400
  },
9208
9401
  buildHistoryRequest: (p) => {
@@ -9233,17 +9426,20 @@ const handleResponses = async (c) => {
9233
9426
  }
9234
9427
  const { vision, initiator } = getResponsesRequestOptions(payload);
9235
9428
  if (state.manualApprove) await awaitApproval();
9429
+ const abort = clientAbortController(c);
9236
9430
  try {
9237
9431
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createResponses(payload, {
9238
9432
  vision,
9239
9433
  initiator,
9240
- resolvedModel: selectedModel
9434
+ resolvedModel: selectedModel,
9435
+ signal: abort.signal
9241
9436
  }));
9242
9437
  ctx.queueWaitMs = queueWaitMs;
9243
9438
  if (isStreamingRequested(payload) && isAsyncIterable(response)) {
9244
9439
  consola.debug("Forwarding native Responses stream");
9245
9440
  updateTrackerStatus(trackingId, "streaming");
9246
9441
  return streamSSE(c, async (stream) => {
9442
+ stream.onAbort(() => abort.abort());
9247
9443
  const idTracker = createStreamIdTracker();
9248
9444
  let finalResult;
9249
9445
  let streamErrorMessage;
@@ -9292,6 +9488,11 @@ const handleResponses = async (c) => {
9292
9488
  completeTracking(trackingId, 0, 0, queueWaitMs, void 0, void 0, ctx.timings);
9293
9489
  } else completeTracking(trackingId, 0, 0, queueWaitMs, void 0, void 0, ctx.timings);
9294
9490
  } catch (error) {
9491
+ if (isAbortError(error)) {
9492
+ consola.debug("[Responses] client disconnected mid-stream; upstream aborted");
9493
+ failTracking(trackingId, "client disconnected");
9494
+ return;
9495
+ }
9295
9496
  recordStreamError({
9296
9497
  acc: { model: finalResult?.model || model },
9297
9498
  fallbackModel: model,
@@ -9327,6 +9528,11 @@ const handleResponses = async (c) => {
9327
9528
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
9328
9529
  return c.json(echoResponseBody(result, ctx));
9329
9530
  } catch (error) {
9531
+ if (isAbortError(error)) {
9532
+ consola.debug("[Responses] client disconnected before response; upstream aborted");
9533
+ failTracking(trackingId, "client disconnected");
9534
+ return new Response(null, { status: 499 });
9535
+ }
9330
9536
  recordErrorResponse(ctx, model, error, "responses", stream);
9331
9537
  failTracking(trackingId, error);
9332
9538
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",