@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.
- package/README.md +1 -4
- package/dist/main.mjs +115 -9
- 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
|
|
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.
|
|
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, {
|
|
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;
|