@dianshuv/copilot-api 0.7.9 → 0.7.10
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/dist/main.mjs +196 -18
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -478,10 +478,128 @@ async function getGitHubUser() {
|
|
|
478
478
|
return await response.json();
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/lib/fetch-retry.ts
|
|
483
|
+
const RETRYABLE_CAUSE_CODES = new Set([
|
|
484
|
+
"UND_ERR_SOCKET",
|
|
485
|
+
"ECONNRESET",
|
|
486
|
+
"ENOTFOUND",
|
|
487
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
488
|
+
]);
|
|
489
|
+
const RETRY_DELAYS_MS = [200, 600];
|
|
490
|
+
function getCauseCode(error) {
|
|
491
|
+
if (!(error instanceof Error)) return void 0;
|
|
492
|
+
const cause = error.cause;
|
|
493
|
+
if (typeof cause !== "object" || cause === null) return void 0;
|
|
494
|
+
const code = cause.code;
|
|
495
|
+
return typeof code === "string" ? code : void 0;
|
|
496
|
+
}
|
|
497
|
+
function isRetryable(error) {
|
|
498
|
+
const code = getCauseCode(error);
|
|
499
|
+
return code !== void 0 && RETRYABLE_CAUSE_CODES.has(code);
|
|
500
|
+
}
|
|
501
|
+
const RETRY_ATTEMPTS_KEY = "__copilotRetryAttempts";
|
|
502
|
+
const RETRY_AUTH_REFRESHED_KEY = "__copilotRetryAuthRefreshed";
|
|
503
|
+
/**
|
|
504
|
+
* Read retry metadata recorded on an error or response by fetchWithRetry.
|
|
505
|
+
* Returns attempts=1 (no retry) if the value is missing or malformed.
|
|
506
|
+
*/
|
|
507
|
+
function getRetryAttempts(target) {
|
|
508
|
+
if (typeof target !== "object" || target === null) return {
|
|
509
|
+
attempts: 1,
|
|
510
|
+
authRefreshed: false
|
|
511
|
+
};
|
|
512
|
+
const meta = target;
|
|
513
|
+
return {
|
|
514
|
+
attempts: typeof meta[RETRY_ATTEMPTS_KEY] === "number" ? meta[RETRY_ATTEMPTS_KEY] : 1,
|
|
515
|
+
authRefreshed: meta[RETRY_AUTH_REFRESHED_KEY] === true
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async function fetchWithRetry(input, init, options) {
|
|
519
|
+
let currentInit = init;
|
|
520
|
+
let authRefreshed = false;
|
|
521
|
+
let networkAttempts = 0;
|
|
522
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) try {
|
|
523
|
+
networkAttempts++;
|
|
524
|
+
const response = await fetch(input, currentInit);
|
|
525
|
+
if (response.status === 401 && !authRefreshed && options?.onUnauthorized) {
|
|
526
|
+
const refreshed = await tryRefreshAuth(options.onUnauthorized);
|
|
527
|
+
if (refreshed) {
|
|
528
|
+
await response.body?.cancel().catch(() => {});
|
|
529
|
+
consola.warn("Got 401 from upstream; refreshed token and retrying");
|
|
530
|
+
currentInit = refreshed;
|
|
531
|
+
authRefreshed = true;
|
|
532
|
+
attempt--;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
annotateRetryMeta(response, networkAttempts, authRefreshed);
|
|
537
|
+
return response;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (attempt === RETRY_DELAYS_MS.length || !isRetryable(error)) {
|
|
540
|
+
annotateRetryMeta(error, networkAttempts, authRefreshed);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
const delay = RETRY_DELAYS_MS[attempt];
|
|
544
|
+
consola.warn(`Upstream network error (${getCauseCode(error)}); retry ${attempt + 1}/${RETRY_DELAYS_MS.length} in ${delay}ms`);
|
|
545
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
546
|
+
}
|
|
547
|
+
throw new Error("fetchWithRetry exhausted attempts without resolution");
|
|
548
|
+
}
|
|
549
|
+
function annotateRetryMeta(target, attempts, authRefreshed) {
|
|
550
|
+
if (typeof target !== "object" || target === null) return;
|
|
551
|
+
try {
|
|
552
|
+
const meta = target;
|
|
553
|
+
meta[RETRY_ATTEMPTS_KEY] = attempts;
|
|
554
|
+
meta[RETRY_AUTH_REFRESHED_KEY] = authRefreshed;
|
|
555
|
+
} catch {}
|
|
556
|
+
}
|
|
557
|
+
async function tryRefreshAuth(onUnauthorized) {
|
|
558
|
+
try {
|
|
559
|
+
return await onUnauthorized();
|
|
560
|
+
} catch (error) {
|
|
561
|
+
consola.warn("onUnauthorized callback failed:", error instanceof Error ? error.message : error);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Build an onUnauthorized callback that refreshes the Copilot token and
|
|
567
|
+
* returns a new RequestInit with an updated Authorization header.
|
|
568
|
+
*/
|
|
569
|
+
function makeCopilotAuthRetry(refreshToken, init) {
|
|
570
|
+
return async () => {
|
|
571
|
+
const newToken = await refreshToken();
|
|
572
|
+
if (!newToken) return null;
|
|
573
|
+
const headers = new Headers(init.headers);
|
|
574
|
+
headers.set("Authorization", `Bearer ${newToken}`);
|
|
575
|
+
return {
|
|
576
|
+
...init,
|
|
577
|
+
headers
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/services/copilot/copilot-fetch.ts
|
|
584
|
+
/**
|
|
585
|
+
* Single transport seam for all upstream Copilot API calls.
|
|
586
|
+
*
|
|
587
|
+
* Wraps every request with:
|
|
588
|
+
* - Network-error retry (UND_ERR_SOCKET, ECONNRESET, ENOTFOUND, connect timeouts)
|
|
589
|
+
* - 401 → forceRefreshCopilotToken → retry once with the new bearer token
|
|
590
|
+
*
|
|
591
|
+
* Callers supply the path (e.g. "/chat/completions") and the usual RequestInit.
|
|
592
|
+
* The host prefix and resilience wiring are applied here so every endpoint
|
|
593
|
+
* gets the same treatment and adding a new endpoint is one call site.
|
|
594
|
+
*/
|
|
595
|
+
function copilotFetch(path, init) {
|
|
596
|
+
return fetchWithRetry(`${copilotBaseUrl(state)}${path}`, init, { onUnauthorized: makeCopilotAuthRetry(forceRefreshCopilotToken, init) });
|
|
597
|
+
}
|
|
598
|
+
|
|
481
599
|
//#endregion
|
|
482
600
|
//#region src/services/copilot/get-models.ts
|
|
483
601
|
const getModels = async () => {
|
|
484
|
-
const response = await
|
|
602
|
+
const response = await copilotFetch("/models", { headers: copilotHeaders(state) });
|
|
485
603
|
if (!response.ok) throw await HTTPError.fromResponse("Failed to get models", response);
|
|
486
604
|
return await response.json();
|
|
487
605
|
};
|
|
@@ -586,6 +704,23 @@ async function refreshCopilotTokenWithRetry(maxRetries = 3) {
|
|
|
586
704
|
return null;
|
|
587
705
|
}
|
|
588
706
|
/**
|
|
707
|
+
* Force-refresh the Copilot token on demand (e.g. after a 401 response).
|
|
708
|
+
* Updates state.copilotToken on success and returns the new token, or null
|
|
709
|
+
* if refresh failed. Coalesces concurrent callers so multiple in-flight 401s
|
|
710
|
+
* only trigger one refresh.
|
|
711
|
+
*/
|
|
712
|
+
let refreshInFlight = null;
|
|
713
|
+
async function forceRefreshCopilotToken() {
|
|
714
|
+
if (refreshInFlight) return refreshInFlight;
|
|
715
|
+
refreshInFlight = refreshCopilotTokenWithRetry().then((token) => {
|
|
716
|
+
if (token) state.copilotToken = token;
|
|
717
|
+
return token;
|
|
718
|
+
}).finally(() => {
|
|
719
|
+
refreshInFlight = null;
|
|
720
|
+
});
|
|
721
|
+
return refreshInFlight;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
589
724
|
* Clear any existing token refresh timer.
|
|
590
725
|
* Call this before setting up a new timer or during cleanup.
|
|
591
726
|
*/
|
|
@@ -1213,7 +1348,7 @@ const patchClaude = defineCommand({
|
|
|
1213
1348
|
|
|
1214
1349
|
//#endregion
|
|
1215
1350
|
//#region package.json
|
|
1216
|
-
var version = "0.7.
|
|
1351
|
+
var version = "0.7.10";
|
|
1217
1352
|
|
|
1218
1353
|
//#endregion
|
|
1219
1354
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -3280,17 +3415,16 @@ const createChatCompletions = async (payload, options) => {
|
|
|
3280
3415
|
const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
|
|
3281
3416
|
const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
3282
3417
|
const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
|
|
3283
|
-
const
|
|
3284
|
-
...copilotHeaders(state, {
|
|
3285
|
-
vision: enableVision && modelSupportsVision,
|
|
3286
|
-
modelRequestHeaders: options?.resolvedModel?.request_headers,
|
|
3287
|
-
intent: isAgentCall ? "conversation-agent" : "conversation-panel"
|
|
3288
|
-
}),
|
|
3289
|
-
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3290
|
-
};
|
|
3291
|
-
const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
|
|
3418
|
+
const response = await copilotFetch("/chat/completions", {
|
|
3292
3419
|
method: "POST",
|
|
3293
|
-
headers
|
|
3420
|
+
headers: {
|
|
3421
|
+
...copilotHeaders(state, {
|
|
3422
|
+
vision: enableVision && modelSupportsVision,
|
|
3423
|
+
modelRequestHeaders: options?.resolvedModel?.request_headers,
|
|
3424
|
+
intent: isAgentCall ? "conversation-agent" : "conversation-panel"
|
|
3425
|
+
}),
|
|
3426
|
+
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3427
|
+
},
|
|
3294
3428
|
body: JSON.stringify(wire)
|
|
3295
3429
|
});
|
|
3296
3430
|
if (!response.ok) {
|
|
@@ -3723,6 +3857,7 @@ function recordErrorResponse(ctx, model, error, endpoint, stream) {
|
|
|
3723
3857
|
}, Date.now() - ctx.startTime);
|
|
3724
3858
|
if (endpoint !== void 0) {
|
|
3725
3859
|
const metrics = extractErrorMetrics(error);
|
|
3860
|
+
const { attempts } = getRetryAttempts(error);
|
|
3726
3861
|
captureRequest({
|
|
3727
3862
|
model,
|
|
3728
3863
|
inputTokens: 0,
|
|
@@ -3734,7 +3869,7 @@ function recordErrorResponse(ctx, model, error, endpoint, stream) {
|
|
|
3734
3869
|
...metrics,
|
|
3735
3870
|
errorPhase: stream ? "pre_stream" : "non_stream",
|
|
3736
3871
|
endpoint,
|
|
3737
|
-
attempt:
|
|
3872
|
+
attempt: attempts
|
|
3738
3873
|
});
|
|
3739
3874
|
}
|
|
3740
3875
|
}
|
|
@@ -4120,7 +4255,24 @@ async function handleStreamingResponse$1(opts) {
|
|
|
4120
4255
|
endpoint: "chat_completions"
|
|
4121
4256
|
});
|
|
4122
4257
|
failTracking(ctx.trackingId, error);
|
|
4123
|
-
|
|
4258
|
+
try {
|
|
4259
|
+
const markerChunk = {
|
|
4260
|
+
id: `error-marker-${Date.now()}`,
|
|
4261
|
+
object: "chat.completion.chunk",
|
|
4262
|
+
created: Math.floor(Date.now() / 1e3),
|
|
4263
|
+
model: acc.model || payload.model,
|
|
4264
|
+
choices: [{
|
|
4265
|
+
index: 0,
|
|
4266
|
+
delta: { content: `\n\n[copilot-api: upstream stream terminated. Please retry.]` },
|
|
4267
|
+
finish_reason: "length",
|
|
4268
|
+
logprobs: null
|
|
4269
|
+
}]
|
|
4270
|
+
};
|
|
4271
|
+
await stream.writeSSE({
|
|
4272
|
+
data: JSON.stringify(markerChunk),
|
|
4273
|
+
event: "message"
|
|
4274
|
+
});
|
|
4275
|
+
} catch {}
|
|
4124
4276
|
}
|
|
4125
4277
|
}
|
|
4126
4278
|
function parseStreamChunk(chunk, acc, checkRepetition) {
|
|
@@ -4223,7 +4375,7 @@ completionRoutes.post("/", async (c) => {
|
|
|
4223
4375
|
//#region src/services/copilot/create-embeddings.ts
|
|
4224
4376
|
const createEmbeddings = async (payload) => {
|
|
4225
4377
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
4226
|
-
const response = await
|
|
4378
|
+
const response = await copilotFetch("/embeddings", {
|
|
4227
4379
|
method: "POST",
|
|
4228
4380
|
headers: copilotHeaders(state),
|
|
4229
4381
|
body: JSON.stringify(payload)
|
|
@@ -4665,6 +4817,16 @@ async function handleGeminiGenerate(c, model, isStream) {
|
|
|
4665
4817
|
endpoint: "chat_completions"
|
|
4666
4818
|
});
|
|
4667
4819
|
failTracking(ctx.trackingId, error);
|
|
4820
|
+
try {
|
|
4821
|
+
await s.write(`data: ${JSON.stringify({ candidates: [{
|
|
4822
|
+
content: {
|
|
4823
|
+
role: "model",
|
|
4824
|
+
parts: [{ text: `\n\n[copilot-api: upstream stream terminated. Please retry.]` }]
|
|
4825
|
+
},
|
|
4826
|
+
finishReason: "OTHER",
|
|
4827
|
+
index: 0
|
|
4828
|
+
}] })}\n\n`);
|
|
4829
|
+
} catch {}
|
|
4668
4830
|
}
|
|
4669
4831
|
});
|
|
4670
4832
|
} catch (error) {
|
|
@@ -6912,7 +7074,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6912
7074
|
}
|
|
6913
7075
|
}
|
|
6914
7076
|
consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
|
|
6915
|
-
const response = await
|
|
7077
|
+
const response = await copilotFetch("/v1/messages", {
|
|
6916
7078
|
method: "POST",
|
|
6917
7079
|
headers,
|
|
6918
7080
|
body: JSON.stringify(filteredPayload)
|
|
@@ -8191,7 +8353,7 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
|
|
|
8191
8353
|
"X-Initiator": initiator
|
|
8192
8354
|
};
|
|
8193
8355
|
payload.service_tier = null;
|
|
8194
|
-
const response = await
|
|
8356
|
+
const response = await copilotFetch("/responses", {
|
|
8195
8357
|
method: "POST",
|
|
8196
8358
|
headers,
|
|
8197
8359
|
body: JSON.stringify(payload)
|
|
@@ -8474,6 +8636,7 @@ const handleResponses = async (c) => {
|
|
|
8474
8636
|
const idTracker = createStreamIdTracker();
|
|
8475
8637
|
let finalResult;
|
|
8476
8638
|
let streamErrorMessage;
|
|
8639
|
+
let lastSequenceNumber = -1;
|
|
8477
8640
|
try {
|
|
8478
8641
|
for await (const chunk of response) {
|
|
8479
8642
|
consola.debug("Responses stream chunk:", JSON.stringify(chunk));
|
|
@@ -8484,6 +8647,10 @@ const handleResponses = async (c) => {
|
|
|
8484
8647
|
if ("response" in parsed) finalResult = parsed.response;
|
|
8485
8648
|
else if (eventType === "error" && "message" in parsed) streamErrorMessage = parsed.message;
|
|
8486
8649
|
} catch {}
|
|
8650
|
+
try {
|
|
8651
|
+
const parsed = JSON.parse(rawData);
|
|
8652
|
+
if (typeof parsed.sequence_number === "number") lastSequenceNumber = parsed.sequence_number;
|
|
8653
|
+
} catch {}
|
|
8487
8654
|
const processedData = fixStreamIds(rawData, eventType, idTracker);
|
|
8488
8655
|
await stream.writeSSE({
|
|
8489
8656
|
id: chunk.id,
|
|
@@ -8522,7 +8689,18 @@ const handleResponses = async (c) => {
|
|
|
8522
8689
|
endpoint: "responses"
|
|
8523
8690
|
});
|
|
8524
8691
|
failTracking(trackingId, error);
|
|
8525
|
-
|
|
8692
|
+
try {
|
|
8693
|
+
await stream.writeSSE({
|
|
8694
|
+
event: "error",
|
|
8695
|
+
data: JSON.stringify({
|
|
8696
|
+
type: "error",
|
|
8697
|
+
code: "upstream_stream_terminated",
|
|
8698
|
+
message: "Upstream stream terminated mid-response. Please retry.",
|
|
8699
|
+
param: null,
|
|
8700
|
+
sequence_number: lastSequenceNumber + 1
|
|
8701
|
+
})
|
|
8702
|
+
});
|
|
8703
|
+
} catch {}
|
|
8526
8704
|
}
|
|
8527
8705
|
});
|
|
8528
8706
|
}
|