@dianshuv/copilot-api 0.11.2 → 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 +191 -34
  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.2";
1351
+ var version = "0.11.4";
1352
1352
 
1353
1353
  //#endregion
1354
1354
  //#region src/lib/adaptive-rate-limiter.ts
@@ -7324,8 +7324,10 @@ function stripServerToolsFromPayload(tools) {
7324
7324
  }
7325
7325
  /**
7326
7326
  * Effective 1M context window. Two ways a request lands on this size:
7327
- * (a) base model id + context-1m-2025-08-07 beta header (e.g. claude-opus-4.8),
7328
- * (b) a distinct upstream "-1m-internal" model id (e.g. claude-opus-4.7-1m-internal).
7327
+ * (a) base model id + context-1m-2025-08-07 beta header (e.g. claude-opus-4.8,
7328
+ * claude-opus-4.7),
7329
+ * (b) a distinct upstream "-1m-internal" model id (a historical convention; no
7330
+ * model currently on Copilot uses it, but the resolver still probes for it).
7329
7331
  */
7330
7332
  const ONE_MILLION_CONTEXT_WINDOW_TOKENS = 1e6;
7331
7333
  /**
@@ -7390,6 +7392,81 @@ function supportsDirectAnthropicApi(modelId) {
7390
7392
  return resolveAnthropicModelForDirectPath(modelId) !== void 0;
7391
7393
  }
7392
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
+
7393
7470
  //#endregion
7394
7471
  //#region src/routes/messages/message-utils.ts
7395
7472
  function convertAnthropicMessages(messages) {
@@ -7677,7 +7754,7 @@ function translateModelName(model) {
7677
7754
  if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
7678
7755
  if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
7679
7756
  if (model === "claude-opus-4-8") return "claude-opus-4.8";
7680
- if (model === "claude-opus-4-7-1m") return "claude-opus-4.7-1m-internal";
7757
+ if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
7681
7758
  if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
7682
7759
  if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
7683
7760
  if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
@@ -8597,27 +8674,66 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
8597
8674
  } else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
8598
8675
  if (state.manualApprove) await awaitApproval();
8599
8676
  const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
8677
+ const isStreaming = anthropicPayload.stream === true;
8600
8678
  try {
8601
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8679
+ const settled = settle(executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
8602
8680
  initiator: initiatorOverride,
8603
8681
  injectContext1mBeta: needsContext1mBeta,
8604
8682
  errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
8605
8683
  clientAnthropicBetaHeader
8606
- }));
8607
- ctx.queueWaitMs = queueWaitMs;
8608
- if (Symbol.asyncIterator in response) {
8609
- consola.debug("Streaming response from Copilot (direct Anthropic)");
8610
- updateTrackerStatus(ctx.trackingId, "streaming");
8611
- return streamSSE(c, async (stream) => {
8612
- await handleDirectAnthropicStreamingResponse({
8613
- stream,
8614
- response,
8615
- anthropicPayload: effectivePayload,
8616
- 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
+ });
8617
8700
  });
8618
- });
8701
+ }
8702
+ return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
8619
8703
  }
8620
- 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
+ });
8621
8737
  } catch (error) {
8622
8738
  if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
8623
8739
  recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
@@ -8862,30 +8978,71 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
8862
8978
  if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
8863
8979
  else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
8864
8980
  else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
8981
+ const isStreaming = anthropicPayload.stream === true;
8865
8982
  try {
8866
- const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8983
+ const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
8867
8984
  initiator: initiatorOverride,
8868
8985
  resolvedModel: selectedModel,
8869
8986
  anthropicBeta,
8870
8987
  errorModelIdOverride
8871
- }));
8872
- ctx.queueWaitMs = queueWaitMs;
8873
- if (isNonStreaming(response)) return handleNonStreamingResponse({
8874
- c,
8875
- response,
8876
- toolNameMapping,
8877
- ctx,
8878
- anthropicPayload
8879
- });
8880
- 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)");
8881
9014
  updateTrackerStatus(ctx.trackingId, "streaming");
8882
9015
  return streamSSE(c, async (stream) => {
8883
- await handleStreamingResponse({
9016
+ await runStreamWithKeepalive({
8884
9017
  stream,
8885
- response,
8886
- toolNameMapping,
8887
- anthropicPayload,
8888
- 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
+ }
8889
9046
  });
8890
9047
  });
8891
9048
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.11.2",
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",