@dianshuv/copilot-api 0.14.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 +217 -11
- 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.
|
|
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:", {
|
|
@@ -6944,6 +6998,74 @@ function supportsDirectAnthropicApi(modelId) {
|
|
|
6944
6998
|
return resolveAnthropicModelForDirectPath(modelId) !== void 0;
|
|
6945
6999
|
}
|
|
6946
7000
|
|
|
7001
|
+
//#endregion
|
|
7002
|
+
//#region src/routes/messages/cache-control-injector.ts
|
|
7003
|
+
function blockHasCacheControl(block) {
|
|
7004
|
+
if (typeof block !== "object" || block === null) return false;
|
|
7005
|
+
if (!("cache_control" in block)) return false;
|
|
7006
|
+
const cc = block.cache_control;
|
|
7007
|
+
return cc !== void 0 && cc !== null;
|
|
7008
|
+
}
|
|
7009
|
+
function toolsHaveCacheControl(tools) {
|
|
7010
|
+
if (!Array.isArray(tools)) return false;
|
|
7011
|
+
return tools.some((t) => blockHasCacheControl(t));
|
|
7012
|
+
}
|
|
7013
|
+
function toolResultInnerHasCacheControl(block) {
|
|
7014
|
+
if (!Array.isArray(block.content)) return false;
|
|
7015
|
+
return block.content.some((inner) => blockHasCacheControl(inner));
|
|
7016
|
+
}
|
|
7017
|
+
function messagesHaveCacheControl(messages) {
|
|
7018
|
+
if (!Array.isArray(messages)) return false;
|
|
7019
|
+
for (const message of messages) {
|
|
7020
|
+
if (!Array.isArray(message.content)) continue;
|
|
7021
|
+
for (const block of message.content) {
|
|
7022
|
+
if (blockHasCacheControl(block)) return true;
|
|
7023
|
+
if (block.type === "tool_result" && toolResultInnerHasCacheControl(block)) return true;
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
return false;
|
|
7027
|
+
}
|
|
7028
|
+
function systemHasCacheControl(system) {
|
|
7029
|
+
if (!Array.isArray(system)) return false;
|
|
7030
|
+
return system.some((b) => blockHasCacheControl(b));
|
|
7031
|
+
}
|
|
7032
|
+
function hasAnyCacheControl(payload) {
|
|
7033
|
+
return systemHasCacheControl(payload.system) || toolsHaveCacheControl(payload.tools) || messagesHaveCacheControl(payload.messages);
|
|
7034
|
+
}
|
|
7035
|
+
function injectSystemCacheControl(payload) {
|
|
7036
|
+
if (hasAnyCacheControl(payload)) return;
|
|
7037
|
+
if (payload.system === void 0 || payload.system === null) return;
|
|
7038
|
+
if (typeof payload.system === "string") {
|
|
7039
|
+
if (payload.system.length === 0) return;
|
|
7040
|
+
payload.system = [{
|
|
7041
|
+
type: "text",
|
|
7042
|
+
text: payload.system,
|
|
7043
|
+
cache_control: { type: "ephemeral" }
|
|
7044
|
+
}];
|
|
7045
|
+
return;
|
|
7046
|
+
}
|
|
7047
|
+
if (payload.system.length === 0) return;
|
|
7048
|
+
const tail = payload.system.at(-1);
|
|
7049
|
+
if (!tail) return;
|
|
7050
|
+
tail.cache_control = { type: "ephemeral" };
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
//#endregion
|
|
7054
|
+
//#region src/routes/messages/date-normalizer.ts
|
|
7055
|
+
const dateTrackingRegex = /(# currentDate\r?\n)Today['’ʼʹ]s date is (\d{4})[/-](\d{2})[/-](\d{2})\.(\r?\n|$)/g;
|
|
7056
|
+
function normalizeClaudeCodeDate(text) {
|
|
7057
|
+
return text.replaceAll(dateTrackingRegex, "$1Today's date is $2-$3-$4.$5");
|
|
7058
|
+
}
|
|
7059
|
+
function normalizeSystemPromptDate(payload) {
|
|
7060
|
+
if (typeof payload.system === "string") {
|
|
7061
|
+
payload.system = normalizeClaudeCodeDate(payload.system);
|
|
7062
|
+
return;
|
|
7063
|
+
}
|
|
7064
|
+
if (Array.isArray(payload.system)) {
|
|
7065
|
+
for (const block of payload.system) if (typeof block.text === "string") block.text = normalizeClaudeCodeDate(block.text);
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
|
|
6947
7069
|
//#endregion
|
|
6948
7070
|
//#region src/lib/stream-keepalive.ts
|
|
6949
7071
|
/** SSE comment line used as a keepalive heartbeat. */
|
|
@@ -8227,13 +8349,15 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
8227
8349
|
} else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
|
|
8228
8350
|
if (state.manualApprove) await awaitApproval();
|
|
8229
8351
|
const clientAnthropicBetaHeader = c.req.header("anthropic-beta");
|
|
8352
|
+
const abort = clientAbortController(c);
|
|
8230
8353
|
const isStreaming = anthropicPayload.stream === true;
|
|
8231
8354
|
try {
|
|
8232
8355
|
const settled = settle(executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
|
|
8233
8356
|
initiator: initiatorOverride,
|
|
8234
8357
|
injectContext1mBeta: needsContext1mBeta,
|
|
8235
8358
|
errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0,
|
|
8236
|
-
clientAnthropicBetaHeader
|
|
8359
|
+
clientAnthropicBetaHeader,
|
|
8360
|
+
signal: abort.signal
|
|
8237
8361
|
})));
|
|
8238
8362
|
const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
|
|
8239
8363
|
if (raced.kind === "error") throw raced.error;
|
|
@@ -8244,6 +8368,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
8244
8368
|
consola.debug("Streaming response from Copilot (direct Anthropic)");
|
|
8245
8369
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8246
8370
|
return streamSSE(c, async (stream) => {
|
|
8371
|
+
stream.onAbort(() => abort.abort());
|
|
8247
8372
|
await handleDirectAnthropicStreamingResponse({
|
|
8248
8373
|
stream,
|
|
8249
8374
|
response,
|
|
@@ -8257,6 +8382,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
8257
8382
|
consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
|
|
8258
8383
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8259
8384
|
return streamSSE(c, async (stream) => {
|
|
8385
|
+
stream.onAbort(() => abort.abort());
|
|
8260
8386
|
await runStreamWithKeepalive({
|
|
8261
8387
|
stream,
|
|
8262
8388
|
settled,
|
|
@@ -8271,6 +8397,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
8271
8397
|
});
|
|
8272
8398
|
},
|
|
8273
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
|
+
}
|
|
8274
8405
|
recordStreamError({
|
|
8275
8406
|
acc: createAnthropicStreamAccumulator(),
|
|
8276
8407
|
fallbackModel: anthropicPayload.model,
|
|
@@ -8288,6 +8419,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
8288
8419
|
});
|
|
8289
8420
|
});
|
|
8290
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
|
+
}
|
|
8291
8427
|
if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
|
|
8292
8428
|
recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
|
|
8293
8429
|
throw error;
|
|
@@ -8442,6 +8578,11 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
8442
8578
|
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8443
8579
|
}, ctx.timings);
|
|
8444
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
|
+
}
|
|
8445
8586
|
consola.error("Direct Anthropic stream error:", formatError(error));
|
|
8446
8587
|
recordStreamError({
|
|
8447
8588
|
acc,
|
|
@@ -8531,13 +8672,15 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
8531
8672
|
if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
|
|
8532
8673
|
else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
|
|
8533
8674
|
else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
|
|
8675
|
+
const abort = clientAbortController(c);
|
|
8534
8676
|
const isStreaming = anthropicPayload.stream === true;
|
|
8535
8677
|
try {
|
|
8536
8678
|
const settled = settle(executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
|
|
8537
8679
|
initiator: initiatorOverride,
|
|
8538
8680
|
resolvedModel: selectedModel,
|
|
8539
8681
|
anthropicBeta,
|
|
8540
|
-
errorModelIdOverride
|
|
8682
|
+
errorModelIdOverride,
|
|
8683
|
+
signal: abort.signal
|
|
8541
8684
|
})));
|
|
8542
8685
|
const raced = isStreaming ? await raceWithGrace(settled, RATE_LIMIT_GRACE_MS) : await settled;
|
|
8543
8686
|
if (raced.kind === "error") throw raced.error;
|
|
@@ -8554,6 +8697,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
8554
8697
|
consola.debug("Streaming response from Copilot");
|
|
8555
8698
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8556
8699
|
return streamSSE(c, async (stream) => {
|
|
8700
|
+
stream.onAbort(() => abort.abort());
|
|
8557
8701
|
await handleStreamingResponse({
|
|
8558
8702
|
stream,
|
|
8559
8703
|
response,
|
|
@@ -8566,6 +8710,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
8566
8710
|
consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
|
|
8567
8711
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8568
8712
|
return streamSSE(c, async (stream) => {
|
|
8713
|
+
stream.onAbort(() => abort.abort());
|
|
8569
8714
|
await runStreamWithKeepalive({
|
|
8570
8715
|
stream,
|
|
8571
8716
|
settled,
|
|
@@ -8582,6 +8727,11 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
8582
8727
|
});
|
|
8583
8728
|
},
|
|
8584
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
|
+
}
|
|
8585
8735
|
recordStreamError({
|
|
8586
8736
|
acc: createAnthropicStreamAccumulator(),
|
|
8587
8737
|
fallbackModel: anthropicPayload.model,
|
|
@@ -8599,6 +8749,11 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
8599
8749
|
});
|
|
8600
8750
|
});
|
|
8601
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
|
+
}
|
|
8602
8757
|
if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
|
|
8603
8758
|
recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
|
|
8604
8759
|
throw error;
|
|
@@ -8687,6 +8842,11 @@ async function handleStreamingResponse(opts) {
|
|
|
8687
8842
|
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8688
8843
|
}, ctx.timings);
|
|
8689
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
|
+
}
|
|
8690
8850
|
consola.error("Stream error:", formatError(error));
|
|
8691
8851
|
recordStreamError({
|
|
8692
8852
|
acc,
|
|
@@ -8805,7 +8965,11 @@ async function handleCompletion(c) {
|
|
|
8805
8965
|
const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
|
|
8806
8966
|
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
8807
8967
|
if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
|
|
8808
|
-
if (supportsDirectAnthropicApi(sanitizedPayload.model))
|
|
8968
|
+
if (supportsDirectAnthropicApi(sanitizedPayload.model)) {
|
|
8969
|
+
normalizeSystemPromptDate(sanitizedPayload);
|
|
8970
|
+
injectSystemCacheControl(sanitizedPayload);
|
|
8971
|
+
return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
8972
|
+
}
|
|
8809
8973
|
return handleTranslatedCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
8810
8974
|
}
|
|
8811
8975
|
/**
|
|
@@ -8938,9 +9102,34 @@ modelRoutes.get("/", async (c) => {
|
|
|
8938
9102
|
}
|
|
8939
9103
|
});
|
|
8940
9104
|
|
|
9105
|
+
//#endregion
|
|
9106
|
+
//#region src/lib/prompt-cache-key.ts
|
|
9107
|
+
const CLIENT_NAME_PATTERNS = [
|
|
9108
|
+
[/^claude[-_]?(?:code|cli)$/i, "claude-code"],
|
|
9109
|
+
[/^codex$/i, "codex"],
|
|
9110
|
+
[/^cursor$/i, "cursor"],
|
|
9111
|
+
[/^aider$/i, "aider"],
|
|
9112
|
+
[/^copilot$/i, "copilot"]
|
|
9113
|
+
];
|
|
9114
|
+
const UA_MAIN_TOKEN = /^([\w.-]+)/;
|
|
9115
|
+
function extractClientName(userAgent) {
|
|
9116
|
+
if (!userAgent) return "unknown";
|
|
9117
|
+
const mainToken = UA_MAIN_TOKEN.exec(userAgent)?.[1]?.toLowerCase();
|
|
9118
|
+
if (!mainToken) return "unknown";
|
|
9119
|
+
for (const [pattern, name] of CLIENT_NAME_PATTERNS) if (pattern.test(mainToken)) return name;
|
|
9120
|
+
return mainToken.slice(0, 32);
|
|
9121
|
+
}
|
|
9122
|
+
function buildPromptCacheKey(clientName) {
|
|
9123
|
+
return `copilot-api:${clientName}`;
|
|
9124
|
+
}
|
|
9125
|
+
function injectPromptCacheKey(payload, clientName) {
|
|
9126
|
+
if (payload.prompt_cache_key !== void 0 && payload.prompt_cache_key !== null && payload.prompt_cache_key !== "") return;
|
|
9127
|
+
payload.prompt_cache_key = buildPromptCacheKey(clientName);
|
|
9128
|
+
}
|
|
9129
|
+
|
|
8941
9130
|
//#endregion
|
|
8942
9131
|
//#region src/services/copilot/create-responses.ts
|
|
8943
|
-
const createResponses = async (payload, { vision, initiator, resolvedModel }) => {
|
|
9132
|
+
const createResponses = async (payload, { vision, initiator, resolvedModel, signal }) => {
|
|
8944
9133
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
8945
9134
|
const modelSupportsVision = resolvedModel?.capabilities?.supports?.vision !== false;
|
|
8946
9135
|
const headers = {
|
|
@@ -8954,7 +9143,8 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
|
|
|
8954
9143
|
const response = await copilotFetch("/responses", {
|
|
8955
9144
|
method: "POST",
|
|
8956
9145
|
headers,
|
|
8957
|
-
body: JSON.stringify(payload)
|
|
9146
|
+
body: JSON.stringify(payload),
|
|
9147
|
+
signal
|
|
8958
9148
|
});
|
|
8959
9149
|
if (!response.ok) {
|
|
8960
9150
|
consola.error("Failed to create responses", response);
|
|
@@ -9195,14 +9385,17 @@ const TERMINAL_EVENTS = new Set([
|
|
|
9195
9385
|
"error"
|
|
9196
9386
|
]);
|
|
9197
9387
|
const handleResponses = async (c) => {
|
|
9388
|
+
const rawPayload = await c.req.json();
|
|
9389
|
+
const clientName = extractClientName(c.req.header("user-agent"));
|
|
9198
9390
|
const { ctx, payload } = createEntryContext({
|
|
9199
9391
|
c,
|
|
9200
|
-
rawPayload
|
|
9392
|
+
rawPayload,
|
|
9201
9393
|
endpoint: "openai",
|
|
9202
9394
|
normalizePayload: (p) => {
|
|
9203
9395
|
const np = state.normalizeResponsesCallIds ? normalizeCallIds(p) : p;
|
|
9204
9396
|
useFunctionApplyPatch(np);
|
|
9205
9397
|
filterUnsupportedBuiltins(np);
|
|
9398
|
+
injectPromptCacheKey(np, clientName);
|
|
9206
9399
|
return np;
|
|
9207
9400
|
},
|
|
9208
9401
|
buildHistoryRequest: (p) => {
|
|
@@ -9233,17 +9426,20 @@ const handleResponses = async (c) => {
|
|
|
9233
9426
|
}
|
|
9234
9427
|
const { vision, initiator } = getResponsesRequestOptions(payload);
|
|
9235
9428
|
if (state.manualApprove) await awaitApproval();
|
|
9429
|
+
const abort = clientAbortController(c);
|
|
9236
9430
|
try {
|
|
9237
9431
|
const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createResponses(payload, {
|
|
9238
9432
|
vision,
|
|
9239
9433
|
initiator,
|
|
9240
|
-
resolvedModel: selectedModel
|
|
9434
|
+
resolvedModel: selectedModel,
|
|
9435
|
+
signal: abort.signal
|
|
9241
9436
|
}));
|
|
9242
9437
|
ctx.queueWaitMs = queueWaitMs;
|
|
9243
9438
|
if (isStreamingRequested(payload) && isAsyncIterable(response)) {
|
|
9244
9439
|
consola.debug("Forwarding native Responses stream");
|
|
9245
9440
|
updateTrackerStatus(trackingId, "streaming");
|
|
9246
9441
|
return streamSSE(c, async (stream) => {
|
|
9442
|
+
stream.onAbort(() => abort.abort());
|
|
9247
9443
|
const idTracker = createStreamIdTracker();
|
|
9248
9444
|
let finalResult;
|
|
9249
9445
|
let streamErrorMessage;
|
|
@@ -9292,6 +9488,11 @@ const handleResponses = async (c) => {
|
|
|
9292
9488
|
completeTracking(trackingId, 0, 0, queueWaitMs, void 0, void 0, ctx.timings);
|
|
9293
9489
|
} else completeTracking(trackingId, 0, 0, queueWaitMs, void 0, void 0, ctx.timings);
|
|
9294
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
|
+
}
|
|
9295
9496
|
recordStreamError({
|
|
9296
9497
|
acc: { model: finalResult?.model || model },
|
|
9297
9498
|
fallbackModel: model,
|
|
@@ -9327,6 +9528,11 @@ const handleResponses = async (c) => {
|
|
|
9327
9528
|
consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
|
|
9328
9529
|
return c.json(echoResponseBody(result, ctx));
|
|
9329
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
|
+
}
|
|
9330
9536
|
recordErrorResponse(ctx, model, error, "responses", stream);
|
|
9331
9537
|
failTracking(trackingId, error);
|
|
9332
9538
|
throw error;
|