@dianshuv/copilot-api 0.11.3 → 0.11.4

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 (2) hide show
  1. package/dist/main.mjs +186 -31
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -1348,7 +1348,7 @@ const patchClaude = defineCommand({
1348
1348
 
1349
1349
  //#endregion
1350
1350
  //#region package.json
1351
- var version = "0.11.3";
1351
+ var version = "0.11.4";
1352
1352
 
1353
1353
  //#endregion
1354
1354
  //#region src/lib/adaptive-rate-limiter.ts
@@ -7392,6 +7392,81 @@ function supportsDirectAnthropicApi(modelId) {
7392
7392
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
7393
7393
  }
7394
7394
 
7395
+ //#endregion
7396
+ //#region src/lib/stream-keepalive.ts
7397
+ /** SSE comment line used as a keepalive heartbeat. */
7398
+ const SSE_PING = ": ping\n\n";
7399
+ /**
7400
+ * Grace period before opening a keepalive stream. Normal upstream responses
7401
+ * resolve sub-second (response headers arrive immediately, the body is not
7402
+ * buffered); only a request queued behind the rate limiter takes >=10s. 3s
7403
+ * cleanly separates the two — a request still pending after 3s is queued.
7404
+ */
7405
+ const RATE_LIMIT_GRACE_MS = 3e3;
7406
+ /**
7407
+ * Interval between keepalive pings once the stream is open. Frequent enough to
7408
+ * prove liveness and defeat idle-connection drops, far below Claude Code's
7409
+ * default 10-minute request timeout.
7410
+ */
7411
+ const KEEPALIVE_PING_INTERVAL_MS = 5e3;
7412
+ /**
7413
+ * Capture a promise's outcome as a value. The returned promise never rejects,
7414
+ * so it can be raced against a timer and awaited again later without producing
7415
+ * an unhandled rejection or a second pending chain on the original promise.
7416
+ */
7417
+ function settle(promise) {
7418
+ return promise.then((value) => ({
7419
+ kind: "done",
7420
+ value
7421
+ }), (error) => ({
7422
+ kind: "error",
7423
+ error
7424
+ }));
7425
+ }
7426
+ /**
7427
+ * Race an already-settled promise against a grace timer. Resolves with the
7428
+ * settled outcome if it arrives within `graceMs`, otherwise `{ kind: "timeout" }`.
7429
+ * The underlying work keeps running — await the SAME `settled` promise afterward
7430
+ * to obtain its eventual result.
7431
+ */
7432
+ async function raceWithGrace(settled, graceMs) {
7433
+ let timer;
7434
+ const timeout = new Promise((resolve) => {
7435
+ timer = setTimeout(() => resolve({ kind: "timeout" }), graceMs);
7436
+ });
7437
+ const result = await Promise.race([settled, timeout]);
7438
+ if (timer !== void 0) clearTimeout(timer);
7439
+ return result;
7440
+ }
7441
+ /**
7442
+ * Drive an open SSE stream while the upstream request is still queued: emit a
7443
+ * ping immediately, then every `pingIntervalMs`, until `settled` resolves —
7444
+ * then hand off to `onResponse` (success) or `onError` (failure). Pings stop on
7445
+ * client abort, on resolution, and never write to an aborted stream.
7446
+ */
7447
+ async function runStreamWithKeepalive(opts) {
7448
+ const { stream, settled, pingIntervalMs, onResponse, onError } = opts;
7449
+ let pinging = true;
7450
+ const sendPing = () => {
7451
+ if (pinging && !stream.aborted) stream.write(SSE_PING);
7452
+ };
7453
+ sendPing();
7454
+ const interval = setInterval(sendPing, pingIntervalMs);
7455
+ stream.onAbort(() => {
7456
+ pinging = false;
7457
+ clearInterval(interval);
7458
+ });
7459
+ let result;
7460
+ try {
7461
+ result = await settled;
7462
+ } finally {
7463
+ pinging = false;
7464
+ clearInterval(interval);
7465
+ }
7466
+ if (result.kind === "done") await onResponse(result.value);
7467
+ else if (result.kind === "error") await onError(result.error);
7468
+ }
7469
+
7395
7470
  //#endregion
7396
7471
  //#region src/routes/messages/message-utils.ts
7397
7472
  function convertAnthropicMessages(messages) {
@@ -8599,27 +8674,66 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8599
8674
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
8600
8675
  if (state.manualApprove) await awaitApproval();
8601
8676
  const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
8677
+ const isStreaming = anthropicPayload.stream === true;
8602
8678
  try {
8603
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8679
+ const settled = settle(executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8604
8680
  initiator: initiatorOverride,
8605
8681
  injectContext1mBeta: needsContext1mBeta,
8606
8682
  errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
8607
8683
  clientAnthropicBetaHeader
8608
- }));
8609
- ctx.queueWaitMs = queueWaitMs;
8610
- if (Symbol.asyncIterator in response) {
8611
- consola.debug("Streaming response from Copilot (direct Anthropic)");
8612
- updateTrackerStatus(ctx.trackingId, "streaming");
8613
- return streamSSE(c, async (stream) => {
8614
- await handleDirectAnthropicStreamingResponse({
8615
- stream,
8616
- response,
8617
- anthropicPayload: effectivePayload,
8618
- ctx
8684
+ })));
8685
+ const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8686
+ if (raced.kind === "error") throw raced.error;
8687
+ if (raced.kind === "done") {
8688
+ const { result: response, queueWaitMs } = raced.value;
8689
+ ctx.queueWaitMs = queueWaitMs;
8690
+ if (Symbol.asyncIterator in response) {
8691
+ consola.debug("Streaming response from Copilot (direct Anthropic)");
8692
+ updateTrackerStatus(ctx.trackingId, "streaming");
8693
+ return streamSSE(c, async (stream) => {
8694
+ await handleDirectAnthropicStreamingResponse({
8695
+ stream,
8696
+ response,
8697
+ anthropicPayload: effectivePayload,
8698
+ ctx
8699
+ });
8619
8700
  });
8620
- });
8701
+ }
8702
+ return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
8621
8703
  }
8622
- return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
8704
+ consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
8705
+ updateTrackerStatus(ctx.trackingId, "streaming");
8706
+ return streamSSE(c, async (stream) => {
8707
+ await runStreamWithKeepalive({
8708
+ stream,
8709
+ settled,
8710
+ pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
8711
+ onResponse: async ({ result: response, queueWaitMs }) => {
8712
+ ctx.queueWaitMs = queueWaitMs;
8713
+ await handleDirectAnthropicStreamingResponse({
8714
+ stream,
8715
+ response,
8716
+ anthropicPayload: effectivePayload,
8717
+ ctx
8718
+ });
8719
+ },
8720
+ onError: async (error) => {
8721
+ recordStreamError({
8722
+ acc: createAnthropicStreamAccumulator(),
8723
+ fallbackModel: anthropicPayload.model,
8724
+ ctx,
8725
+ error,
8726
+ endpoint: "messages"
8727
+ });
8728
+ failTracking(ctx.trackingId, error);
8729
+ const errorEvent = translateErrorToAnthropicErrorEvent(error);
8730
+ await stream.writeSSE({
8731
+ event: errorEvent.type,
8732
+ data: JSON.stringify(errorEvent)
8733
+ });
8734
+ }
8735
+ });
8736
+ });
8623
8737
  } catch (error) {
8624
8738
  if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
8625
8739
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
@@ -8864,30 +8978,71 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8864
8978
  if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8865
8979
  else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8866
8980
  else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8981
+ const isStreaming = anthropicPayload.stream === true;
8867
8982
  try {
8868
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8983
+ const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8869
8984
  initiator: initiatorOverride,
8870
8985
  resolvedModel: selectedModel,
8871
8986
  anthropicBeta,
8872
8987
  errorModelIdOverride
8873
- }));
8874
- ctx.queueWaitMs = queueWaitMs;
8875
- if (isNonStreaming(response)) return handleNonStreamingResponse({
8876
- c,
8877
- response,
8878
- toolNameMapping,
8879
- ctx,
8880
- anthropicPayload
8881
- });
8882
- consola.debug("Streaming response from Copilot");
8988
+ })));
8989
+ const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
8990
+ if (raced.kind === "error") throw raced.error;
8991
+ if (raced.kind === "done") {
8992
+ const { result: response, queueWaitMs } = raced.value;
8993
+ ctx.queueWaitMs = queueWaitMs;
8994
+ if (isNonStreaming(response)) return handleNonStreamingResponse({
8995
+ c,
8996
+ response,
8997
+ toolNameMapping,
8998
+ ctx,
8999
+ anthropicPayload
9000
+ });
9001
+ consola.debug("Streaming response from Copilot");
9002
+ updateTrackerStatus(ctx.trackingId, "streaming");
9003
+ return streamSSE(c, async (stream) => {
9004
+ await handleStreamingResponse({
9005
+ stream,
9006
+ response,
9007
+ toolNameMapping,
9008
+ anthropicPayload,
9009
+ ctx
9010
+ });
9011
+ });
9012
+ }
9013
+ consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
8883
9014
  updateTrackerStatus(ctx.trackingId, "streaming");
8884
9015
  return streamSSE(c, async (stream) => {
8885
- await handleStreamingResponse({
9016
+ await runStreamWithKeepalive({
8886
9017
  stream,
8887
- response,
8888
- toolNameMapping,
8889
- anthropicPayload,
8890
- ctx
9018
+ settled,
9019
+ pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
9020
+ onResponse: async ({ result: response, queueWaitMs }) => {
9021
+ ctx.queueWaitMs = queueWaitMs;
9022
+ if (isNonStreaming(response)) return;
9023
+ await handleStreamingResponse({
9024
+ stream,
9025
+ response,
9026
+ toolNameMapping,
9027
+ anthropicPayload,
9028
+ ctx
9029
+ });
9030
+ },
9031
+ onError: async (error) => {
9032
+ recordStreamError({
9033
+ acc: createAnthropicStreamAccumulator(),
9034
+ fallbackModel: anthropicPayload.model,
9035
+ ctx,
9036
+ error,
9037
+ endpoint: "messages"
9038
+ });
9039
+ failTracking(ctx.trackingId, error);
9040
+ const errorEvent = translateErrorToAnthropicErrorEvent(error);
9041
+ await stream.writeSSE({
9042
+ event: errorEvent.type,
9043
+ data: JSON.stringify(errorEvent)
9044
+ });
9045
+ }
8891
9046
  });
8892
9047
  });
8893
9048
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",