@dianshuv/copilot-api 0.7.9 → 0.8.0
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 +386 -32
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -243,7 +243,7 @@ function compressToolResultContent(content) {
|
|
|
243
243
|
return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
|
|
244
244
|
}
|
|
245
245
|
function calculateLimits(model, config, defaultContextWindow) {
|
|
246
|
-
const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? defaultContextWindow;
|
|
246
|
+
const rawTokenLimit = getEffectiveTokenLimit(config.tokenLimitCacheKeyOverride ?? model.id) ?? config.contextWindowOverride ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? defaultContextWindow;
|
|
247
247
|
return {
|
|
248
248
|
tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
|
|
249
249
|
byteLimit: getEffectiveByteLimitBytes()
|
|
@@ -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.
|
|
1351
|
+
var version = "0.8.0";
|
|
1217
1352
|
|
|
1218
1353
|
//#endregion
|
|
1219
1354
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -3260,9 +3395,66 @@ const getTokenCount = async (payload, model) => {
|
|
|
3260
3395
|
};
|
|
3261
3396
|
};
|
|
3262
3397
|
|
|
3398
|
+
//#endregion
|
|
3399
|
+
//#region src/lib/anthropic/beta.ts
|
|
3400
|
+
/**
|
|
3401
|
+
* Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
|
|
3402
|
+
*
|
|
3403
|
+
* Lives in `lib/anthropic/` (not in either transport module) so both the
|
|
3404
|
+
* Anthropic-native and OpenAI-translated transport layers can share these
|
|
3405
|
+
* helpers without introducing cross-transport imports.
|
|
3406
|
+
*/
|
|
3407
|
+
/** Anthropic beta feature that unlocks the 1M context window. */
|
|
3408
|
+
const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
|
|
3409
|
+
/**
|
|
3410
|
+
* Merge two comma-separated anthropic-beta header values. Trims whitespace,
|
|
3411
|
+
* drops empty tokens, and dedupes by exact string match. Returns a canonical
|
|
3412
|
+
* comma-joined string with no spaces.
|
|
3413
|
+
*
|
|
3414
|
+
* Either input may be undefined / empty.
|
|
3415
|
+
*/
|
|
3416
|
+
function mergeBetaFeatures(existing, incoming) {
|
|
3417
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3418
|
+
const out = [];
|
|
3419
|
+
for (const raw of [existing, incoming]) {
|
|
3420
|
+
if (!raw) continue;
|
|
3421
|
+
for (const part of raw.split(",")) {
|
|
3422
|
+
const f = part.trim();
|
|
3423
|
+
if (f.length === 0 || seen.has(f)) continue;
|
|
3424
|
+
seen.add(f);
|
|
3425
|
+
out.push(f);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
return out.join(",");
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Append the context-1m feature to an anthropic-beta header value, deduping
|
|
3432
|
+
* any prior occurrence. Returns the merged comma-separated string.
|
|
3433
|
+
*/
|
|
3434
|
+
function appendContext1mBeta(existing) {
|
|
3435
|
+
return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* True iff a model id appears to be the suffixed 1M-context variant of an
|
|
3439
|
+
* Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
|
|
3440
|
+
*
|
|
3441
|
+
* Used as a state.models-independent signal for whether to inject the
|
|
3442
|
+
* context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
|
|
3443
|
+
* empty model cache (where `resolveAnthropicModelForDirectPath` would return
|
|
3444
|
+
* undefined). Forwarding the beta is harmless to upstreams that ignore it.
|
|
3445
|
+
*/
|
|
3446
|
+
function isOneMillionSuffixedClaudeId(modelId) {
|
|
3447
|
+
return modelId.startsWith("claude-") && modelId.endsWith("-1m");
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3263
3450
|
//#endregion
|
|
3264
3451
|
//#region src/services/copilot/create-chat-completions.ts
|
|
3265
3452
|
const GPT_MODEL_PATTERN = /^gpt-/i;
|
|
3453
|
+
/** Case-insensitive lookup of a header key in a plain-object header bag. */
|
|
3454
|
+
function findHeaderKey(headers, name) {
|
|
3455
|
+
const lower = name.toLowerCase();
|
|
3456
|
+
return Object.keys(headers).find((k) => k.toLowerCase() === lower);
|
|
3457
|
+
}
|
|
3266
3458
|
const createChatCompletions = async (payload, options) => {
|
|
3267
3459
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
3268
3460
|
const vendor = options?.resolvedModel?.vendor;
|
|
@@ -3288,14 +3480,19 @@ const createChatCompletions = async (payload, options) => {
|
|
|
3288
3480
|
}),
|
|
3289
3481
|
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3290
3482
|
};
|
|
3291
|
-
|
|
3483
|
+
if (options?.anthropicBeta) {
|
|
3484
|
+
const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
|
|
3485
|
+
headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
|
|
3486
|
+
consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
|
|
3487
|
+
}
|
|
3488
|
+
const response = await copilotFetch("/chat/completions", {
|
|
3292
3489
|
method: "POST",
|
|
3293
3490
|
headers,
|
|
3294
3491
|
body: JSON.stringify(wire)
|
|
3295
3492
|
});
|
|
3296
3493
|
if (!response.ok) {
|
|
3297
3494
|
consola.error("Failed to create chat completions", response);
|
|
3298
|
-
throw await HTTPError.fromResponse("Failed to create chat completions", response, payload.model);
|
|
3495
|
+
throw await HTTPError.fromResponse("Failed to create chat completions", response, options?.errorModelIdOverride ?? payload.model);
|
|
3299
3496
|
}
|
|
3300
3497
|
if (payload.stream) return events(response);
|
|
3301
3498
|
return await response.json();
|
|
@@ -3723,6 +3920,7 @@ function recordErrorResponse(ctx, model, error, endpoint, stream) {
|
|
|
3723
3920
|
}, Date.now() - ctx.startTime);
|
|
3724
3921
|
if (endpoint !== void 0) {
|
|
3725
3922
|
const metrics = extractErrorMetrics(error);
|
|
3923
|
+
const { attempts } = getRetryAttempts(error);
|
|
3726
3924
|
captureRequest({
|
|
3727
3925
|
model,
|
|
3728
3926
|
inputTokens: 0,
|
|
@@ -3734,7 +3932,7 @@ function recordErrorResponse(ctx, model, error, endpoint, stream) {
|
|
|
3734
3932
|
...metrics,
|
|
3735
3933
|
errorPhase: stream ? "pre_stream" : "non_stream",
|
|
3736
3934
|
endpoint,
|
|
3737
|
-
attempt:
|
|
3935
|
+
attempt: attempts
|
|
3738
3936
|
});
|
|
3739
3937
|
}
|
|
3740
3938
|
}
|
|
@@ -3829,7 +4027,7 @@ function isNonStreaming(response) {
|
|
|
3829
4027
|
return Object.hasOwn(response, "choices");
|
|
3830
4028
|
}
|
|
3831
4029
|
/** Build final payload with auto-truncate if needed */
|
|
3832
|
-
async function buildFinalPayload(payload, model) {
|
|
4030
|
+
async function buildFinalPayload(payload, model, autoTruncateConfig = {}) {
|
|
3833
4031
|
if (!state.autoTruncate || !model) {
|
|
3834
4032
|
if (state.autoTruncate && !model) consola.warn(`Auto-truncate: Model '${payload.model}' not found in cached models, skipping`);
|
|
3835
4033
|
return {
|
|
@@ -3838,7 +4036,7 @@ async function buildFinalPayload(payload, model) {
|
|
|
3838
4036
|
};
|
|
3839
4037
|
}
|
|
3840
4038
|
try {
|
|
3841
|
-
const check = await checkNeedsCompactionOpenAI(payload, model);
|
|
4039
|
+
const check = await checkNeedsCompactionOpenAI(payload, model, autoTruncateConfig);
|
|
3842
4040
|
consola.debug(`Auto-truncate check: ${check.currentTokens} tokens (limit ${check.tokenLimit}), ${Math.round(check.currentBytes / 1024)}KB (limit ${check.byteLimit === Infinity ? "unlimited" : `${Math.round(check.byteLimit / 1024)}KB`}), needed: ${check.needed}${check.reason ? ` (${check.reason})` : ""}`);
|
|
3843
4041
|
if (!check.needed) return {
|
|
3844
4042
|
finalPayload: payload,
|
|
@@ -3849,7 +4047,7 @@ async function buildFinalPayload(payload, model) {
|
|
|
3849
4047
|
else if (check.reason === "bytes") reasonText = "size";
|
|
3850
4048
|
else reasonText = "tokens";
|
|
3851
4049
|
consola.info(`Auto-truncate triggered: exceeds ${reasonText} limit`);
|
|
3852
|
-
const truncateResult = await autoTruncateOpenAI(payload, model);
|
|
4050
|
+
const truncateResult = await autoTruncateOpenAI(payload, model, autoTruncateConfig);
|
|
3853
4051
|
return {
|
|
3854
4052
|
finalPayload: truncateResult.payload,
|
|
3855
4053
|
truncateResult
|
|
@@ -4120,7 +4318,24 @@ async function handleStreamingResponse$1(opts) {
|
|
|
4120
4318
|
endpoint: "chat_completions"
|
|
4121
4319
|
});
|
|
4122
4320
|
failTracking(ctx.trackingId, error);
|
|
4123
|
-
|
|
4321
|
+
try {
|
|
4322
|
+
const markerChunk = {
|
|
4323
|
+
id: `error-marker-${Date.now()}`,
|
|
4324
|
+
object: "chat.completion.chunk",
|
|
4325
|
+
created: Math.floor(Date.now() / 1e3),
|
|
4326
|
+
model: acc.model || payload.model,
|
|
4327
|
+
choices: [{
|
|
4328
|
+
index: 0,
|
|
4329
|
+
delta: { content: `\n\n[copilot-api: upstream stream terminated. Please retry.]` },
|
|
4330
|
+
finish_reason: "length",
|
|
4331
|
+
logprobs: null
|
|
4332
|
+
}]
|
|
4333
|
+
};
|
|
4334
|
+
await stream.writeSSE({
|
|
4335
|
+
data: JSON.stringify(markerChunk),
|
|
4336
|
+
event: "message"
|
|
4337
|
+
});
|
|
4338
|
+
} catch {}
|
|
4124
4339
|
}
|
|
4125
4340
|
}
|
|
4126
4341
|
function parseStreamChunk(chunk, acc, checkRepetition) {
|
|
@@ -4223,7 +4438,7 @@ completionRoutes.post("/", async (c) => {
|
|
|
4223
4438
|
//#region src/services/copilot/create-embeddings.ts
|
|
4224
4439
|
const createEmbeddings = async (payload) => {
|
|
4225
4440
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
4226
|
-
const response = await
|
|
4441
|
+
const response = await copilotFetch("/embeddings", {
|
|
4227
4442
|
method: "POST",
|
|
4228
4443
|
headers: copilotHeaders(state),
|
|
4229
4444
|
body: JSON.stringify(payload)
|
|
@@ -4665,6 +4880,16 @@ async function handleGeminiGenerate(c, model, isStream) {
|
|
|
4665
4880
|
endpoint: "chat_completions"
|
|
4666
4881
|
});
|
|
4667
4882
|
failTracking(ctx.trackingId, error);
|
|
4883
|
+
try {
|
|
4884
|
+
await s.write(`data: ${JSON.stringify({ candidates: [{
|
|
4885
|
+
content: {
|
|
4886
|
+
role: "model",
|
|
4887
|
+
parts: [{ text: `\n\n[copilot-api: upstream stream terminated. Please retry.]` }]
|
|
4888
|
+
},
|
|
4889
|
+
finishReason: "OTHER",
|
|
4890
|
+
index: 0
|
|
4891
|
+
}] })}\n\n`);
|
|
4892
|
+
} catch {}
|
|
4668
4893
|
}
|
|
4669
4894
|
});
|
|
4670
4895
|
} catch (error) {
|
|
@@ -6677,7 +6902,7 @@ function modelSupportsContextEditing(modelId) {
|
|
|
6677
6902
|
}
|
|
6678
6903
|
function modelSupportsToolSearch(modelId) {
|
|
6679
6904
|
const n = normalizeForMatching(modelId);
|
|
6680
|
-
return n.includes("claude") && (n.includes("opus45") || n.includes("opus46") || n.includes("sonnet45") || n.includes("sonnet46"));
|
|
6905
|
+
return n.includes("claude") && (n.includes("opus45") || n.includes("opus46") || n.includes("opus47") || n.includes("opus48") || n.includes("sonnet45") || n.includes("sonnet46"));
|
|
6681
6906
|
}
|
|
6682
6907
|
function isContextEditingEnabled(modelId) {
|
|
6683
6908
|
return modelSupportsContextEditing(modelId) && state.contextEditingMode !== "off";
|
|
@@ -6903,6 +7128,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6903
7128
|
};
|
|
6904
7129
|
const betaHeaders = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel);
|
|
6905
7130
|
Object.assign(headers, betaHeaders);
|
|
7131
|
+
if (options?.injectContext1mBeta) headers["anthropic-beta"] = appendContext1mBeta(headers["anthropic-beta"]);
|
|
6906
7132
|
if (isContextEditingEnabled(filteredPayload.model)) {
|
|
6907
7133
|
const hasThinking = filteredPayload.thinking?.type === "enabled";
|
|
6908
7134
|
const cm = buildContextManagement(state.contextEditingMode, hasThinking);
|
|
@@ -6912,7 +7138,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6912
7138
|
}
|
|
6913
7139
|
}
|
|
6914
7140
|
consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
|
|
6915
|
-
const response = await
|
|
7141
|
+
const response = await copilotFetch("/v1/messages", {
|
|
6916
7142
|
method: "POST",
|
|
6917
7143
|
headers,
|
|
6918
7144
|
body: JSON.stringify(filteredPayload)
|
|
@@ -6929,7 +7155,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6929
7155
|
thinking: filteredPayload.thinking,
|
|
6930
7156
|
messageCount: filteredPayload.messages.length
|
|
6931
7157
|
});
|
|
6932
|
-
throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, filteredPayload.model);
|
|
7158
|
+
throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, options?.errorModelIdOverride ?? filteredPayload.model);
|
|
6933
7159
|
}
|
|
6934
7160
|
if (payload.stream) return events(response);
|
|
6935
7161
|
return await response.json();
|
|
@@ -6952,13 +7178,58 @@ function stripServerToolsFromPayload(tools) {
|
|
|
6952
7178
|
}
|
|
6953
7179
|
return result.length > 0 ? result : void 0;
|
|
6954
7180
|
}
|
|
7181
|
+
/** Context window unlocked by the context-1m-2025-08-07 beta header. */
|
|
7182
|
+
const ONE_MILLION_CONTEXT_WINDOW_TOKENS = 1e6;
|
|
7183
|
+
/**
|
|
7184
|
+
* Convert a Claude model id from the client-facing dash convention to the
|
|
7185
|
+
* upstream Copilot dot convention. The two conventions co-exist because
|
|
7186
|
+
* Anthropic-style clients use dashes ("claude-opus-4-8") and Copilot lists
|
|
7187
|
+
* the same model with dots ("claude-opus-4.8"). Only the first dash inside
|
|
7188
|
+
* the version segment is converted (a model id like "claude-opus-4.8-1m"
|
|
7189
|
+
* already in dot form is returned unchanged).
|
|
7190
|
+
*/
|
|
7191
|
+
function dashToDotClaudeId(modelId) {
|
|
7192
|
+
if (!modelId.startsWith("claude-")) return modelId;
|
|
7193
|
+
return modelId.replace(/^(claude-[a-z]+-)(\d+)-(\d+)/, "$1$2.$3");
|
|
7194
|
+
}
|
|
7195
|
+
function resolveAnthropicModelForDirectPath(modelId) {
|
|
7196
|
+
const exact = findModelById(modelId);
|
|
7197
|
+
if (exact?.vendor === "Anthropic") return {
|
|
7198
|
+
model: exact,
|
|
7199
|
+
baseModelId: modelId,
|
|
7200
|
+
oneMillionFallback: false,
|
|
7201
|
+
effectiveContextWindowTokens: exact.capabilities?.limits?.max_context_window_tokens ?? 2e5
|
|
7202
|
+
};
|
|
7203
|
+
const dotted = dashToDotClaudeId(modelId);
|
|
7204
|
+
if (dotted !== modelId) {
|
|
7205
|
+
const dottedExact = findModelById(dotted);
|
|
7206
|
+
if (dottedExact?.vendor === "Anthropic") return {
|
|
7207
|
+
model: dottedExact,
|
|
7208
|
+
baseModelId: dotted,
|
|
7209
|
+
oneMillionFallback: false,
|
|
7210
|
+
effectiveContextWindowTokens: dottedExact.capabilities?.limits?.max_context_window_tokens ?? 2e5
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7213
|
+
if (modelId.endsWith("-1m")) {
|
|
7214
|
+
const baseId = modelId.slice(0, -3);
|
|
7215
|
+
for (const candidateId of [baseId, dashToDotClaudeId(baseId)]) {
|
|
7216
|
+
const base = findModelById(candidateId);
|
|
7217
|
+
if (base?.vendor === "Anthropic") return {
|
|
7218
|
+
model: base,
|
|
7219
|
+
baseModelId: candidateId,
|
|
7220
|
+
oneMillionFallback: true,
|
|
7221
|
+
effectiveContextWindowTokens: ONE_MILLION_CONTEXT_WINDOW_TOKENS
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
7225
|
+
}
|
|
6955
7226
|
/**
|
|
6956
7227
|
* Check if a model supports direct Anthropic API.
|
|
6957
7228
|
* Returns true if redirect is disabled (direct API is on) and the model is from Anthropic vendor.
|
|
6958
7229
|
*/
|
|
6959
7230
|
function supportsDirectAnthropicApi(modelId) {
|
|
6960
7231
|
if (state.redirectAnthropic) return false;
|
|
6961
|
-
return
|
|
7232
|
+
return resolveAnthropicModelForDirectPath(modelId) !== void 0;
|
|
6962
7233
|
}
|
|
6963
7234
|
|
|
6964
7235
|
//#endregion
|
|
@@ -7197,18 +7468,42 @@ function findLatestModel(familyPrefix, fallback) {
|
|
|
7197
7468
|
const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
|
|
7198
7469
|
if (candidates.length === 0) return fallback;
|
|
7199
7470
|
candidates.sort((a, b) => {
|
|
7200
|
-
const
|
|
7201
|
-
|
|
7471
|
+
const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
|
|
7472
|
+
const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
|
|
7473
|
+
if (aMajor !== bMajor) return bMajor - aMajor;
|
|
7474
|
+
return bMinor - aMinor;
|
|
7202
7475
|
});
|
|
7203
7476
|
return candidates[0].id;
|
|
7204
7477
|
}
|
|
7205
7478
|
/**
|
|
7206
|
-
* Extract numeric version from model
|
|
7207
|
-
*
|
|
7479
|
+
* Extract numeric [major, minor] version from a model id.
|
|
7480
|
+
*
|
|
7481
|
+
* Supports both naming conventions Anthropic/Copilot have used:
|
|
7482
|
+
* - dot: "claude-opus-4.5" → [4, 5]
|
|
7483
|
+
* - dash: "claude-opus-4-8" → [4, 8]
|
|
7484
|
+
* - dash double-digit: "claude-opus-4-10" → [4, 10]
|
|
7485
|
+
*
|
|
7486
|
+
* The dash form previously parsed as just the major via the regex
|
|
7487
|
+
* /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
|
|
7488
|
+
* downgraded dash-named candidates against any dot-named candidate in
|
|
7489
|
+
* findLatestModel. Parsing into a tuple also avoids the parseFloat
|
|
7490
|
+
* lossiness on double-digit minors ("4.10" → 4.1).
|
|
7491
|
+
*
|
|
7492
|
+
* Anything after the major/minor segment (date stamps, "-1m") is ignored.
|
|
7493
|
+
* The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
|
|
7494
|
+
* directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
|
|
7495
|
+
* for a minor version of 20_250_514 — without that bound, dated ids would
|
|
7496
|
+
* outrank legitimate dotted candidates like "claude-opus-4.8" in
|
|
7497
|
+
* findLatestModel's sort.
|
|
7498
|
+
*
|
|
7499
|
+
* Returns [0, 0] when no version can be extracted.
|
|
7208
7500
|
*/
|
|
7209
7501
|
function extractVersion(modelId, prefix) {
|
|
7210
|
-
const match = modelId.slice(prefix.length + 1).match(/^(\d+(
|
|
7211
|
-
|
|
7502
|
+
const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
|
|
7503
|
+
if (!match) return [0, 0];
|
|
7504
|
+
const major = Number.parseInt(match[1], 10);
|
|
7505
|
+
const rawMinor = match[2];
|
|
7506
|
+
return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
|
|
7212
7507
|
}
|
|
7213
7508
|
function translateModelName(model) {
|
|
7214
7509
|
const aliasMap = {
|
|
@@ -7222,6 +7517,8 @@ function translateModelName(model) {
|
|
|
7222
7517
|
}
|
|
7223
7518
|
if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
|
|
7224
7519
|
if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
|
|
7520
|
+
if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
|
|
7521
|
+
if (model === "claude-opus-4-8") return "claude-opus-4.8";
|
|
7225
7522
|
if (model === "claude-opus-4-7-1m") return "claude-opus-4.7-1m-internal";
|
|
7226
7523
|
if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
|
|
7227
7524
|
if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
|
|
@@ -7601,14 +7898,30 @@ function translateErrorToAnthropicErrorEvent(error) {
|
|
|
7601
7898
|
*/
|
|
7602
7899
|
async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride) {
|
|
7603
7900
|
consola.debug("Using direct Anthropic API path for model:", anthropicPayload.model);
|
|
7604
|
-
const
|
|
7605
|
-
|
|
7901
|
+
const resolution = resolveAnthropicModelForDirectPath(anthropicPayload.model);
|
|
7902
|
+
const selectedModel = resolution?.model;
|
|
7903
|
+
const baseModelId = resolution?.baseModelId ?? anthropicPayload.model;
|
|
7904
|
+
const needsContext1mBeta = resolution?.oneMillionFallback ?? false;
|
|
7905
|
+
const originalModelId = anthropicPayload.model;
|
|
7906
|
+
const resolvedPayload = resolution && baseModelId !== originalModelId ? {
|
|
7907
|
+
...anthropicPayload,
|
|
7908
|
+
model: baseModelId
|
|
7909
|
+
} : anthropicPayload;
|
|
7910
|
+
if (baseModelId !== originalModelId) {
|
|
7911
|
+
consola.debug(`[Anthropic] Mapping model for upstream: ${originalModelId} → ${baseModelId}${needsContext1mBeta ? " (+context-1m beta)" : ""}`);
|
|
7912
|
+
updateTrackerResolvedModel(ctx.trackingId, baseModelId);
|
|
7913
|
+
}
|
|
7914
|
+
const autoTruncateConfig = resolution && resolution.oneMillionFallback ? {
|
|
7915
|
+
contextWindowOverride: resolution.effectiveContextWindowTokens,
|
|
7916
|
+
tokenLimitCacheKeyOverride: originalModelId
|
|
7917
|
+
} : {};
|
|
7918
|
+
let effectivePayload = resolvedPayload;
|
|
7606
7919
|
let truncateResult;
|
|
7607
7920
|
if (state.autoTruncate && selectedModel) {
|
|
7608
|
-
const check = await checkNeedsCompactionAnthropic(
|
|
7921
|
+
const check = await checkNeedsCompactionAnthropic(resolvedPayload, selectedModel, autoTruncateConfig);
|
|
7609
7922
|
consola.debug(`[Anthropic] Auto-truncate check: ${check.currentTokens} tokens (limit ${check.tokenLimit}), ${Math.round(check.currentBytes / 1024)}KB (limit ${check.byteLimit === Infinity ? "unlimited" : `${Math.round(check.byteLimit / 1024)}KB`}), needed: ${check.needed}${check.reason ? ` (${check.reason})` : ""}`);
|
|
7610
7923
|
if (check.needed) try {
|
|
7611
|
-
truncateResult = await autoTruncateAnthropic(
|
|
7924
|
+
truncateResult = await autoTruncateAnthropic(resolvedPayload, selectedModel, autoTruncateConfig);
|
|
7612
7925
|
if (truncateResult.wasCompacted) effectivePayload = truncateResult.payload;
|
|
7613
7926
|
} catch (error) {
|
|
7614
7927
|
consola.warn("[Anthropic] Auto-truncate failed, proceeding with original payload:", error instanceof Error ? error.message : error);
|
|
@@ -7616,7 +7929,11 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
7616
7929
|
} else if (state.autoTruncate && !selectedModel) consola.debug(`[Anthropic] Model '${anthropicPayload.model}' not found, skipping auto-truncate`);
|
|
7617
7930
|
if (state.manualApprove) await awaitApproval();
|
|
7618
7931
|
try {
|
|
7619
|
-
const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
|
|
7932
|
+
const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createAnthropicMessages(effectivePayload, {
|
|
7933
|
+
initiator: initiatorOverride,
|
|
7934
|
+
injectContext1mBeta: needsContext1mBeta,
|
|
7935
|
+
errorModelIdOverride: needsContext1mBeta ? originalModelId : void 0
|
|
7936
|
+
}));
|
|
7620
7937
|
ctx.queueWaitMs = queueWaitMs;
|
|
7621
7938
|
if (Symbol.asyncIterator in response) {
|
|
7622
7939
|
consola.debug("Streaming response from Copilot (direct Anthropic)");
|
|
@@ -7814,17 +8131,33 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
|
|
|
7814
8131
|
* Handle completion using OpenAI translation path (legacy)
|
|
7815
8132
|
*/
|
|
7816
8133
|
async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride) {
|
|
8134
|
+
const originalModelId = anthropicPayload.model;
|
|
8135
|
+
const hasOneMillionSuffix = isOneMillionSuffixedClaudeId(originalModelId);
|
|
8136
|
+
const oneMResolution = resolveAnthropicModelForDirectPath(originalModelId);
|
|
8137
|
+
const needsContext1mBeta = hasOneMillionSuffix || (oneMResolution?.oneMillionFallback ?? false);
|
|
7817
8138
|
const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
|
|
7818
8139
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
|
|
7819
8140
|
updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
|
|
7820
8141
|
const selectedModel = findModelById(translatedPayload.model);
|
|
7821
|
-
const
|
|
8142
|
+
const autoTruncateConfig = oneMResolution?.oneMillionFallback ? {
|
|
8143
|
+
contextWindowOverride: oneMResolution.effectiveContextWindowTokens,
|
|
8144
|
+
tokenLimitCacheKeyOverride: originalModelId
|
|
8145
|
+
} : {};
|
|
8146
|
+
const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel, autoTruncateConfig);
|
|
7822
8147
|
if (truncateResult) ctx.truncateResult = truncateResult;
|
|
8148
|
+
let anthropicBeta = c.req.header("anthropic-beta");
|
|
8149
|
+
if (needsContext1mBeta) anthropicBeta = appendContext1mBeta(anthropicBeta);
|
|
7823
8150
|
if (state.manualApprove) await awaitApproval();
|
|
8151
|
+
let errorModelIdOverride;
|
|
8152
|
+
if (autoTruncateConfig.tokenLimitCacheKeyOverride !== void 0) errorModelIdOverride = autoTruncateConfig.tokenLimitCacheKeyOverride;
|
|
8153
|
+
else if (needsContext1mBeta && selectedModel) errorModelIdOverride = selectedModel.id;
|
|
8154
|
+
else if (hasOneMillionSuffix) errorModelIdOverride = originalModelId;
|
|
7824
8155
|
try {
|
|
7825
8156
|
const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(openAIPayload, {
|
|
7826
8157
|
initiator: initiatorOverride,
|
|
7827
|
-
resolvedModel: selectedModel
|
|
8158
|
+
resolvedModel: selectedModel,
|
|
8159
|
+
anthropicBeta,
|
|
8160
|
+
errorModelIdOverride
|
|
7828
8161
|
}));
|
|
7829
8162
|
ctx.queueWaitMs = queueWaitMs;
|
|
7830
8163
|
if (isNonStreaming(response)) return handleNonStreamingResponse({
|
|
@@ -8092,10 +8425,15 @@ async function handleCountTokens(c) {
|
|
|
8092
8425
|
consola.warn("Model not found, returning default token count");
|
|
8093
8426
|
return c.json({ input_tokens: 1 });
|
|
8094
8427
|
}
|
|
8428
|
+
const directResolution = resolveAnthropicModelForDirectPath(anthropicPayload.model);
|
|
8429
|
+
const autoTruncateConfig = directResolution?.oneMillionFallback ? {
|
|
8430
|
+
contextWindowOverride: directResolution.effectiveContextWindowTokens,
|
|
8431
|
+
tokenLimitCacheKeyOverride: anthropicPayload.model
|
|
8432
|
+
} : {};
|
|
8095
8433
|
if (state.autoTruncate) {
|
|
8096
|
-
const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel);
|
|
8434
|
+
const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel, autoTruncateConfig);
|
|
8097
8435
|
if (truncateCheck.needed) {
|
|
8098
|
-
const contextWindow = selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
|
|
8436
|
+
const contextWindow = autoTruncateConfig.contextWindowOverride ?? selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
|
|
8099
8437
|
const inflatedTokens = Math.floor(contextWindow * .95);
|
|
8100
8438
|
consola.debug(`[count_tokens] Would trigger auto-truncate: ${truncateCheck.currentTokens} tokens > ${truncateCheck.tokenLimit}, returning inflated count: ${inflatedTokens}`);
|
|
8101
8439
|
return c.json({ input_tokens: inflatedTokens });
|
|
@@ -8191,7 +8529,7 @@ const createResponses = async (payload, { vision, initiator, resolvedModel }) =>
|
|
|
8191
8529
|
"X-Initiator": initiator
|
|
8192
8530
|
};
|
|
8193
8531
|
payload.service_tier = null;
|
|
8194
|
-
const response = await
|
|
8532
|
+
const response = await copilotFetch("/responses", {
|
|
8195
8533
|
method: "POST",
|
|
8196
8534
|
headers,
|
|
8197
8535
|
body: JSON.stringify(payload)
|
|
@@ -8474,6 +8812,7 @@ const handleResponses = async (c) => {
|
|
|
8474
8812
|
const idTracker = createStreamIdTracker();
|
|
8475
8813
|
let finalResult;
|
|
8476
8814
|
let streamErrorMessage;
|
|
8815
|
+
let lastSequenceNumber = -1;
|
|
8477
8816
|
try {
|
|
8478
8817
|
for await (const chunk of response) {
|
|
8479
8818
|
consola.debug("Responses stream chunk:", JSON.stringify(chunk));
|
|
@@ -8484,6 +8823,10 @@ const handleResponses = async (c) => {
|
|
|
8484
8823
|
if ("response" in parsed) finalResult = parsed.response;
|
|
8485
8824
|
else if (eventType === "error" && "message" in parsed) streamErrorMessage = parsed.message;
|
|
8486
8825
|
} catch {}
|
|
8826
|
+
try {
|
|
8827
|
+
const parsed = JSON.parse(rawData);
|
|
8828
|
+
if (typeof parsed.sequence_number === "number") lastSequenceNumber = parsed.sequence_number;
|
|
8829
|
+
} catch {}
|
|
8487
8830
|
const processedData = fixStreamIds(rawData, eventType, idTracker);
|
|
8488
8831
|
await stream.writeSSE({
|
|
8489
8832
|
id: chunk.id,
|
|
@@ -8522,7 +8865,18 @@ const handleResponses = async (c) => {
|
|
|
8522
8865
|
endpoint: "responses"
|
|
8523
8866
|
});
|
|
8524
8867
|
failTracking(trackingId, error);
|
|
8525
|
-
|
|
8868
|
+
try {
|
|
8869
|
+
await stream.writeSSE({
|
|
8870
|
+
event: "error",
|
|
8871
|
+
data: JSON.stringify({
|
|
8872
|
+
type: "error",
|
|
8873
|
+
code: "upstream_stream_terminated",
|
|
8874
|
+
message: "Upstream stream terminated mid-response. Please retry.",
|
|
8875
|
+
param: null,
|
|
8876
|
+
sequence_number: lastSequenceNumber + 1
|
|
8877
|
+
})
|
|
8878
|
+
});
|
|
8879
|
+
} catch {}
|
|
8526
8880
|
}
|
|
8527
8881
|
});
|
|
8528
8882
|
}
|