@dianshuv/copilot-api 0.15.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 +115 -9
  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.15.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:", {
@@ -8295,13 +8349,15 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8295
8349
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
8296
8350
  if (state.manualApprove) await awaitApproval();
8297
8351
  const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
8352
+ const abort = clientAbortController(c);
8298
8353
  const isStreaming = anthropicPayload.stream === true;
8299
8354
  try {
8300
8355
  const settled = settle(executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8301
8356
  initiator: initiatorOverride,
8302
8357
  injectContext1mBeta: needsContext1mBeta,
8303
8358
  errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
8304
- clientAnthropicBetaHeader
8359
+ clientAnthropicBetaHeader,
8360
+ signal: abort.signal
8305
8361
  })));
8306
8362
  const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8307
8363
  if (raced.kind === "error") throw raced.error;
@@ -8312,6 +8368,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8312
8368
  consola.debug("Streaming response from Copilot (direct Anthropic)");
8313
8369
  updateTrackerStatus(ctx.trackingId, "streaming");
8314
8370
  return streamSSE(c, async (stream) => {
8371
+ stream.onAbort(() => abort.abort());
8315
8372
  await handleDirectAnthropicStreamingResponse({
8316
8373
  stream,
8317
8374
  response,
@@ -8325,6 +8382,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8325
8382
  consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
8326
8383
  updateTrackerStatus(ctx.trackingId, "streaming");
8327
8384
  return streamSSE(c, async (stream) => {
8385
+ stream.onAbort(() => abort.abort());
8328
8386
  await runStreamWithKeepalive({
8329
8387
  stream,
8330
8388
  settled,
@@ -8339,6 +8397,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8339
8397
  });
8340
8398
  },
8341
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
+ }
8342
8405
  recordStreamError({
8343
8406
  acc: createAnthropicStreamAccumulator(),
8344
8407
  fallbackModel: anthropicPayload.model,
@@ -8356,6 +8419,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8356
8419
  });
8357
8420
  });
8358
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
+ }
8359
8427
  if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
8360
8428
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
8361
8429
  throw error;
@@ -8510,6 +8578,11 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8510
8578
  toolCount: anthropicPayload.tools?.length ?? 0
8511
8579
  }, ctx.timings);
8512
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
+ }
8513
8586
  consola.error("Direct Anthropic stream error:", formatError(error));
8514
8587
  recordStreamError({
8515
8588
  acc,
@@ -8599,13 +8672,15 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8599
8672
  if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8600
8673
  else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8601
8674
  else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8675
+ const abort = clientAbortController(c);
8602
8676
  const isStreaming = anthropicPayload.stream === true;
8603
8677
  try {
8604
8678
  const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8605
8679
  initiator: initiatorOverride,
8606
8680
  resolvedModel: selectedModel,
8607
8681
  anthropicBeta,
8608
- errorModelIdOverride
8682
+ errorModelIdOverride,
8683
+ signal: abort.signal
8609
8684
  })));
8610
8685
  const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8611
8686
  if (raced.kind === "error") throw raced.error;
@@ -8622,6 +8697,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8622
8697
  consola.debug("Streaming response from Copilot");
8623
8698
  updateTrackerStatus(ctx.trackingId, "streaming");
8624
8699
  return streamSSE(c, async (stream) => {
8700
+ stream.onAbort(() => abort.abort());
8625
8701
  await handleStreamingResponse({
8626
8702
  stream,
8627
8703
  response,
@@ -8634,6 +8710,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8634
8710
  consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
8635
8711
  updateTrackerStatus(ctx.trackingId, "streaming");
8636
8712
  return streamSSE(c, async (stream) => {
8713
+ stream.onAbort(() => abort.abort());
8637
8714
  await runStreamWithKeepalive({
8638
8715
  stream,
8639
8716
  settled,
@@ -8650,6 +8727,11 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8650
8727
  });
8651
8728
  },
8652
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
+ }
8653
8735
  recordStreamError({
8654
8736
  acc: createAnthropicStreamAccumulator(),
8655
8737
  fallbackModel: anthropicPayload.model,
@@ -8667,6 +8749,11 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8667
8749
  });
8668
8750
  });
8669
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
+ }
8670
8757
  if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
8671
8758
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
8672
8759
  throw error;
@@ -8755,6 +8842,11 @@ async function handleStreamingResponse(opts) {
8755
8842
  toolCount: anthropicPayload.tools?.length ?? 0
8756
8843
  }, ctx.timings);
8757
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
+ }
8758
8850
  consola.error("Stream error:", formatError(error));
8759
8851
  recordStreamError({
8760
8852
  acc,
@@ -9037,7 +9129,7 @@ function injectPromptCacheKey(payload, clientName) {
9037
9129
 
9038
9130
  //#endregion
9039
9131
  //#region src/services/copilot/create-responses.ts
9040
- const createResponses = async (payload, { vision, initiator, resolvedModel }) => {
9132
+ const createResponses = async (payload, { vision, initiator, resolvedModel, signal }) => {
9041
9133
  if (!state.copilotToken) throw new Error("Copilot token not found");
9042
9134
  const modelSupportsVision = resolvedModel?.capabilities?.supports?.vision !== false;
9043
9135
  const headers = {
@@ -9051,7 +9143,8 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
9051
9143
  const response = await copilotFetch("/responses", {
9052
9144
  method: "POST",
9053
9145
  headers,
9054
- body: JSON.stringify(payload)
9146
+ body: JSON.stringify(payload),
9147
+ signal
9055
9148
  });
9056
9149
  if (!response.ok) {
9057
9150
  consola.error("Failed to create responses", response);
@@ -9333,17 +9426,20 @@ const handleResponses = async (c) => {
9333
9426
  }
9334
9427
  const { vision, initiator } = getResponsesRequestOptions(payload);
9335
9428
  if (state.manualApprove) await awaitApproval();
9429
+ const abort = clientAbortController(c);
9336
9430
  try {
9337
9431
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createResponses(payload, {
9338
9432
  vision,
9339
9433
  initiator,
9340
- resolvedModel: selectedModel
9434
+ resolvedModel: selectedModel,
9435
+ signal: abort.signal
9341
9436
  }));
9342
9437
  ctx.queueWaitMs = queueWaitMs;
9343
9438
  if (isStreamingRequested(payload) && isAsyncIterable(response)) {
9344
9439
  consola.debug("Forwarding native Responses stream");
9345
9440
  updateTrackerStatus(trackingId, "streaming");
9346
9441
  return streamSSE(c, async (stream) => {
9442
+ stream.onAbort(() => abort.abort());
9347
9443
  const idTracker = createStreamIdTracker();
9348
9444
  let finalResult;
9349
9445
  let streamErrorMessage;
@@ -9392,6 +9488,11 @@ const handleResponses = async (c) => {
9392
9488
  completeTracking(trackingId, 0, 0, queueWaitMs, void 0, void 0, ctx.timings);
9393
9489
  } else completeTracking(trackingId, 0, 0, queueWaitMs, void 0, void 0, ctx.timings);
9394
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
+ }
9395
9496
  recordStreamError({
9396
9497
  acc: { model: finalResult?.model || model },
9397
9498
  fallbackModel: model,
@@ -9427,6 +9528,11 @@ const handleResponses = async (c) => {
9427
9528
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
9428
9529
  return c.json(echoResponseBody(result, ctx));
9429
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
+ }
9430
9536
  recordErrorResponse(ctx, model, error, "responses", stream);
9431
9537
  failTracking(trackingId, error);
9432
9538
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.15.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",