copilot-api-plus 1.2.57 → 1.3.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/{account-manager-DUSOybLm.js → account-manager-C6u4XrO1.js} +7 -4
- package/dist/account-manager-C6u4XrO1.js.map +1 -0
- package/dist/error-DO0tIuq0.js +3 -0
- package/dist/get-user-Brre_x1N.js +3 -0
- package/dist/main.js +560 -15
- package/dist/main.js.map +1 -1
- package/dist/{token-Dhk6Zi-n.js → token-DOQwaFAc.js} +2 -2
- package/dist/{token-Dhk6Zi-n.js.map → token-DOQwaFAc.js.map} +1 -1
- package/dist/token-Dv2Iyk1S.js +4 -0
- package/package.json +1 -1
- package/dist/account-manager-DUSOybLm.js.map +0 -1
- package/dist/error-HsNBNEyW.js +0 -3
- package/dist/get-user-DiK-7IaC.js +0 -3
- package/dist/token-C8PZ2fm8.js +0 -4
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, accountManager, cacheModels, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, findModel, forwardError, getAccountDispatcher, getCopilotUsage, initProxyFromEnv, isAccountProxied, isNullish, isProxyActive, notifyStreamEnd, notifyStreamStart, resetAccountConnections, resetConnections, rootCause, sleep, standardHeaders, state } from "./account-manager-
|
|
3
|
-
import { clearGithubToken, getDeviceCode, pollAccessToken, refreshCopilotToken, setupCopilotToken, setupGitHubToken, stopCopilotTokenRefresh } from "./token-
|
|
2
|
+
import { GITHUB_BASE_URL, GITHUB_CLIENT_ID, HTTPError, PATHS, accountManager, cacheModels, cacheVSCodeVersion, copilotBaseUrl, copilotHeaders, ensurePaths, findModel, forwardError, getAccountDispatcher, getCopilotUsage, initProxyFromEnv, isAccountProxied, isNullish, isProxyActive, notifyStreamEnd, notifyStreamStart, resetAccountConnections, resetConnections, rootCause, sleep, standardHeaders, state } from "./account-manager-C6u4XrO1.js";
|
|
3
|
+
import { clearGithubToken, getDeviceCode, pollAccessToken, refreshCopilotToken, setupCopilotToken, setupGitHubToken, stopCopilotTokenRefresh } from "./token-DOQwaFAc.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
6
|
import consola from "consola";
|
|
@@ -1932,7 +1932,7 @@ function stripOpenAIReminders(payload) {
|
|
|
1932
1932
|
* Copilot's slow models (e.g. claude-opus with thinking) can take up to
|
|
1933
1933
|
* ~120s to start streaming, so we give a generous timeout for headers.
|
|
1934
1934
|
*/
|
|
1935
|
-
const FETCH_TIMEOUT_MS = 12e4;
|
|
1935
|
+
const FETCH_TIMEOUT_MS$1 = 12e4;
|
|
1936
1936
|
/** Minimum interval (ms) between requests on the same account. */
|
|
1937
1937
|
const MIN_SAME_ACCOUNT_INTERVAL_MS = 1e3;
|
|
1938
1938
|
/** Random jitter range (ms) added when switching between accounts. */
|
|
@@ -1946,7 +1946,7 @@ let lastUsedAccountId;
|
|
|
1946
1946
|
* arrive – once the body starts streaming, the timeout is cleared so that
|
|
1947
1947
|
* long SSE responses are not interrupted.
|
|
1948
1948
|
*/
|
|
1949
|
-
async function fetchWithTimeout(url, init, { timeoutMs = FETCH_TIMEOUT_MS, accountId, accountProxy } = {}) {
|
|
1949
|
+
async function fetchWithTimeout$1(url, init, { timeoutMs = FETCH_TIMEOUT_MS$1, accountId, accountProxy } = {}) {
|
|
1950
1950
|
const controller = new AbortController();
|
|
1951
1951
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1952
1952
|
try {
|
|
@@ -1976,8 +1976,8 @@ async function fetchWithTimeout(url, init, { timeoutMs = FETCH_TIMEOUT_MS, accou
|
|
|
1976
1976
|
*/
|
|
1977
1977
|
async function fetchWithRetry(url, buildInit, { accountId, accountProxy } = {}) {
|
|
1978
1978
|
try {
|
|
1979
|
-
return await fetchWithTimeout(url, buildInit(), {
|
|
1980
|
-
timeoutMs: FETCH_TIMEOUT_MS,
|
|
1979
|
+
return await fetchWithTimeout$1(url, buildInit(), {
|
|
1980
|
+
timeoutMs: FETCH_TIMEOUT_MS$1,
|
|
1981
1981
|
accountId,
|
|
1982
1982
|
accountProxy
|
|
1983
1983
|
});
|
|
@@ -2241,9 +2241,9 @@ async function retryWithModifiedPayload(payload, releaseSlot) {
|
|
|
2241
2241
|
* Dispatch request to either single-account or multi-account path.
|
|
2242
2242
|
*/
|
|
2243
2243
|
function dispatchRequest(payload) {
|
|
2244
|
-
return state.multiAccountEnabled && accountManager.hasAccounts() ? createWithMultiAccount(payload) : createWithSingleAccount(payload);
|
|
2244
|
+
return state.multiAccountEnabled && accountManager.hasAccounts() ? createWithMultiAccount$1(payload) : createWithSingleAccount$1(payload);
|
|
2245
2245
|
}
|
|
2246
|
-
async function createWithSingleAccount(payload) {
|
|
2246
|
+
async function createWithSingleAccount$1(payload) {
|
|
2247
2247
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
2248
2248
|
const enableVision = payload.messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
|
|
2249
2249
|
const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
@@ -2270,7 +2270,7 @@ async function createWithSingleAccount(payload) {
|
|
|
2270
2270
|
consola.warn("Copilot token expired, refreshing and retrying...");
|
|
2271
2271
|
try {
|
|
2272
2272
|
await refreshCopilotToken();
|
|
2273
|
-
response = await fetchWithTimeout(url, {
|
|
2273
|
+
response = await fetchWithTimeout$1(url, {
|
|
2274
2274
|
method: "POST",
|
|
2275
2275
|
headers: buildHeaders(),
|
|
2276
2276
|
body: bodyString
|
|
@@ -2422,7 +2422,7 @@ function recordBreakerFailure(reason) {
|
|
|
2422
2422
|
consola.warn(`Circuit breaker OPEN for ${CB_OPEN_MS / 1e3}s after ${breaker.failures} consecutive failures (last: ${reason})`);
|
|
2423
2423
|
}
|
|
2424
2424
|
}
|
|
2425
|
-
async function createWithMultiAccount(payload) {
|
|
2425
|
+
async function createWithMultiAccount$1(payload) {
|
|
2426
2426
|
const remaining = breakerOpenRemainingMs();
|
|
2427
2427
|
if (remaining > 0) throw new HTTPError("Upstream temporarily unavailable", new Response(JSON.stringify({ error: {
|
|
2428
2428
|
type: "service_unavailable",
|
|
@@ -3157,6 +3157,405 @@ async function handleCountTokens(c) {
|
|
|
3157
3157
|
}
|
|
3158
3158
|
}
|
|
3159
3159
|
|
|
3160
|
+
//#endregion
|
|
3161
|
+
//#region src/lib/anthropic-sanitizer.ts
|
|
3162
|
+
/** Upstream message that triggers the assistant-thinking-strip retry. */
|
|
3163
|
+
const INVALID_THINKING_SIGNATURE_PATTERN = /invalid [`'"]?signature[`'"]? in [`'"]?thinking[`'"]? block/i;
|
|
3164
|
+
function isRecord(value) {
|
|
3165
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3166
|
+
}
|
|
3167
|
+
/**
|
|
3168
|
+
* Strip fields the Copilot backend rejects.
|
|
3169
|
+
*
|
|
3170
|
+
* Mutates the payload in place.
|
|
3171
|
+
*/
|
|
3172
|
+
function sanitizeForCopilotBackend(payload) {
|
|
3173
|
+
const extended = payload;
|
|
3174
|
+
if ("context_management" in extended) {
|
|
3175
|
+
consola.debug("Stripping context_management (unsupported by Copilot backend)");
|
|
3176
|
+
delete extended.context_management;
|
|
3177
|
+
}
|
|
3178
|
+
sanitizeOutputConfigFormat(extended.output_config?.format);
|
|
3179
|
+
}
|
|
3180
|
+
function sanitizeOutputConfigFormat(format) {
|
|
3181
|
+
if (!isRecord(format) || format.type !== "json_schema") return;
|
|
3182
|
+
const nested = isRecord(format.json_schema) ? format.json_schema : void 0;
|
|
3183
|
+
const hasFlat = isRecord(format.schema);
|
|
3184
|
+
const hasNested = isRecord(nested?.schema);
|
|
3185
|
+
if (!hasFlat && hasNested) format.schema = nested.schema;
|
|
3186
|
+
if ("json_schema" in format) {
|
|
3187
|
+
consola.debug("Flattening output_config.format.json_schema → format.schema");
|
|
3188
|
+
delete format.json_schema;
|
|
3189
|
+
}
|
|
3190
|
+
if ("name" in format) {
|
|
3191
|
+
consola.debug("Stripping output_config.format.name (Copilot reject)");
|
|
3192
|
+
delete format.name;
|
|
3193
|
+
}
|
|
3194
|
+
if ("strict" in format) {
|
|
3195
|
+
consola.debug("Stripping output_config.format.strict (Copilot reject)");
|
|
3196
|
+
delete format.strict;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Adaptive thinking has a slightly different shape than enabled thinking;
|
|
3201
|
+
* Copilot rejects `budget_tokens_max`. Mutates in place.
|
|
3202
|
+
*/
|
|
3203
|
+
function normalizeAdaptiveThinkingForCopilot(payload) {
|
|
3204
|
+
const thinking = payload.thinking;
|
|
3205
|
+
if (!isRecord(thinking) || thinking.type !== "adaptive") return;
|
|
3206
|
+
if ("budget_tokens_max" in thinking) {
|
|
3207
|
+
consola.debug("Stripping budget_tokens_max from adaptive thinking (Copilot reject)");
|
|
3208
|
+
delete thinking.budget_tokens_max;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Remove all `thinking` and `redacted_thinking` blocks from assistant
|
|
3213
|
+
* messages, and drop any assistant turns left empty as a result.
|
|
3214
|
+
*
|
|
3215
|
+
* Pure — returns a new payload, never mutates the input.
|
|
3216
|
+
*/
|
|
3217
|
+
function stripAssistantThinkingBlocks(payload) {
|
|
3218
|
+
let strippedBlocks = 0;
|
|
3219
|
+
let droppedAssistantMessages = 0;
|
|
3220
|
+
const messages = payload.messages.flatMap((message) => {
|
|
3221
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return [message];
|
|
3222
|
+
const content = message.content.filter((block) => {
|
|
3223
|
+
const shouldStrip = block.type === "thinking" || block.type === "redacted_thinking";
|
|
3224
|
+
if (shouldStrip) strippedBlocks += 1;
|
|
3225
|
+
return !shouldStrip;
|
|
3226
|
+
});
|
|
3227
|
+
if (content.length === message.content.length) return [message];
|
|
3228
|
+
if (content.length === 0) {
|
|
3229
|
+
droppedAssistantMessages += 1;
|
|
3230
|
+
return [];
|
|
3231
|
+
}
|
|
3232
|
+
return [{
|
|
3233
|
+
...message,
|
|
3234
|
+
content
|
|
3235
|
+
}];
|
|
3236
|
+
});
|
|
3237
|
+
if (strippedBlocks === 0) return {
|
|
3238
|
+
payload,
|
|
3239
|
+
stripped: false,
|
|
3240
|
+
strippedBlocks: 0,
|
|
3241
|
+
droppedAssistantMessages: 0
|
|
3242
|
+
};
|
|
3243
|
+
return {
|
|
3244
|
+
payload: {
|
|
3245
|
+
...payload,
|
|
3246
|
+
messages
|
|
3247
|
+
},
|
|
3248
|
+
stripped: true,
|
|
3249
|
+
strippedBlocks,
|
|
3250
|
+
droppedAssistantMessages
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
/** Detect the upstream "invalid thinking signature" 400 to trigger retry. */
|
|
3254
|
+
async function isInvalidThinkingSignatureError(error) {
|
|
3255
|
+
if (!(error instanceof HTTPError) || error.response.status !== 400) return false;
|
|
3256
|
+
const message = await readUpstreamErrorMessage(error.response);
|
|
3257
|
+
return typeof message === "string" && INVALID_THINKING_SIGNATURE_PATTERN.test(message);
|
|
3258
|
+
}
|
|
3259
|
+
async function readUpstreamErrorMessage(response) {
|
|
3260
|
+
let text;
|
|
3261
|
+
try {
|
|
3262
|
+
text = await response.clone().text();
|
|
3263
|
+
} catch {
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
if (!text) return void 0;
|
|
3267
|
+
try {
|
|
3268
|
+
return extractErrorMessage(JSON.parse(text)) ?? text;
|
|
3269
|
+
} catch {
|
|
3270
|
+
return text;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
function extractErrorMessage(payload) {
|
|
3274
|
+
if (!isRecord(payload)) return void 0;
|
|
3275
|
+
if (typeof payload.message === "string") return payload.message;
|
|
3276
|
+
const errorField = payload.error;
|
|
3277
|
+
if (isRecord(errorField) && typeof errorField.message === "string") return errorField.message;
|
|
3278
|
+
}
|
|
3279
|
+
function overrideAnthropicResponseModel(response, requestedModel) {
|
|
3280
|
+
return {
|
|
3281
|
+
...response,
|
|
3282
|
+
model: requestedModel
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Override the `model` field in a `message_start` SSE event payload.
|
|
3287
|
+
* Returns the original JSON string if the event is not a message_start
|
|
3288
|
+
* or cannot be parsed.
|
|
3289
|
+
*/
|
|
3290
|
+
function overrideMessageStartEventModel(rawData, requestedModel) {
|
|
3291
|
+
try {
|
|
3292
|
+
const parsed = JSON.parse(rawData);
|
|
3293
|
+
if (parsed.type !== "message_start" || !parsed.message) return rawData;
|
|
3294
|
+
parsed.message.model = requestedModel;
|
|
3295
|
+
return JSON.stringify(parsed);
|
|
3296
|
+
} catch {
|
|
3297
|
+
return rawData;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
//#endregion
|
|
3302
|
+
//#region src/lib/route-resolver.ts
|
|
3303
|
+
/**
|
|
3304
|
+
* Heuristic fallback when the model has no `supported_endpoints` field
|
|
3305
|
+
* (e.g. older Copilot deployments or a stripped models list). We only
|
|
3306
|
+
* use this when the wire-level capability is missing — never to override
|
|
3307
|
+
* an explicitly-advertised capability.
|
|
3308
|
+
*
|
|
3309
|
+
* Anthropic-published Claude models all natively support /v1/messages on
|
|
3310
|
+
* the Copilot backend at the time of writing. Non-Claude models do not.
|
|
3311
|
+
*/
|
|
3312
|
+
function looksLikeClaudeModel(model) {
|
|
3313
|
+
return /^claude-/i.test(model);
|
|
3314
|
+
}
|
|
3315
|
+
/** Per-process cache of the routing decision keyed by model id. */
|
|
3316
|
+
const routeCache = /* @__PURE__ */ new Map();
|
|
3317
|
+
/**
|
|
3318
|
+
* Resolve the upstream route for an Anthropic /v1/messages payload.
|
|
3319
|
+
*
|
|
3320
|
+
* Order of precedence:
|
|
3321
|
+
* 1. User force-disabled native passthrough — always translate.
|
|
3322
|
+
* 2. Model advertises `anthropic-messages` in supported_endpoints → native.
|
|
3323
|
+
* 3. Model advertises supported_endpoints WITHOUT anthropic-messages → translate.
|
|
3324
|
+
* 4. Capability missing → fall back to name heuristic (claude-* → native).
|
|
3325
|
+
*/
|
|
3326
|
+
function resolveAnthropicRoute(model) {
|
|
3327
|
+
if (state.disableAnthropicPassthrough) return "translate-openai";
|
|
3328
|
+
const cached = routeCache.get(model);
|
|
3329
|
+
if (cached) return cached;
|
|
3330
|
+
const decision = decideRoute(model);
|
|
3331
|
+
routeCache.set(model, decision);
|
|
3332
|
+
return decision;
|
|
3333
|
+
}
|
|
3334
|
+
function decideRoute(model) {
|
|
3335
|
+
const endpoints = findModel(model)?.supported_endpoints;
|
|
3336
|
+
if (Array.isArray(endpoints) && endpoints.length > 0) return endpoints.includes("anthropic-messages") ? "native-anthropic" : "translate-openai";
|
|
3337
|
+
return looksLikeClaudeModel(model) ? "native-anthropic" : "translate-openai";
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
//#endregion
|
|
3341
|
+
//#region src/services/copilot/create-anthropic-messages.ts
|
|
3342
|
+
const FETCH_TIMEOUT_MS = 12e4;
|
|
3343
|
+
async function fetchWithTimeout(url, init, { timeoutMs = FETCH_TIMEOUT_MS, accountId, accountProxy, externalSignal } = {}) {
|
|
3344
|
+
const controller = new AbortController();
|
|
3345
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3346
|
+
const onExternalAbort = () => controller.abort();
|
|
3347
|
+
if (externalSignal) if (externalSignal.aborted) controller.abort();
|
|
3348
|
+
else externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
3349
|
+
try {
|
|
3350
|
+
const fetchOptions = {
|
|
3351
|
+
...init,
|
|
3352
|
+
signal: controller.signal
|
|
3353
|
+
};
|
|
3354
|
+
if (accountId) fetchOptions.dispatcher = getAccountDispatcher(accountId, accountProxy);
|
|
3355
|
+
return await fetch(url, fetchOptions);
|
|
3356
|
+
} catch (error) {
|
|
3357
|
+
if (error instanceof DOMException && error.name === "AbortError") throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
3358
|
+
throw error;
|
|
3359
|
+
} finally {
|
|
3360
|
+
clearTimeout(timer);
|
|
3361
|
+
if (externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
function messageContainsVisionInput(message) {
|
|
3365
|
+
if (message.role !== "user" || !Array.isArray(message.content)) return false;
|
|
3366
|
+
return message.content.some((block) => block.type === "image" || block.type === "tool_result" && toolResultContainsImage(block));
|
|
3367
|
+
}
|
|
3368
|
+
function toolResultContainsImage(block) {
|
|
3369
|
+
if (!Array.isArray(block.content)) return false;
|
|
3370
|
+
return block.content.some((contentBlock) => contentBlock.type === "image");
|
|
3371
|
+
}
|
|
3372
|
+
function messageContinuesAgentLoop(message) {
|
|
3373
|
+
if (message.role === "assistant") return true;
|
|
3374
|
+
if (!Array.isArray(message.content)) return false;
|
|
3375
|
+
return message.content.some((block) => block.type === "tool_result");
|
|
3376
|
+
}
|
|
3377
|
+
function buildAnthropicHeaders(payload, source, options$1) {
|
|
3378
|
+
const enableVision = payload.messages.some((m) => messageContainsVisionInput(m));
|
|
3379
|
+
const isAgentCall = payload.messages.some((m) => messageContinuesAgentLoop(m));
|
|
3380
|
+
return {
|
|
3381
|
+
...copilotHeaders(source, enableVision),
|
|
3382
|
+
"X-Initiator": isAgentCall ? "agent" : "user",
|
|
3383
|
+
...options$1?.anthropicBeta ? { "anthropic-beta": options$1.anthropicBeta } : {}
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
async function createAnthropicMessages(payload, options$1) {
|
|
3387
|
+
sanitizeForCopilotBackend(payload);
|
|
3388
|
+
normalizeAdaptiveThinkingForCopilot(payload);
|
|
3389
|
+
try {
|
|
3390
|
+
return await dispatchAnthropicRequest(payload, options$1);
|
|
3391
|
+
} catch (error) {
|
|
3392
|
+
if (!await isInvalidThinkingSignatureError(error)) throw error;
|
|
3393
|
+
const stripped = stripAssistantThinkingBlocks(payload);
|
|
3394
|
+
if (!stripped.stripped) throw error;
|
|
3395
|
+
const droppedSuffix = stripped.droppedAssistantMessages > 0 ? ` and dropping ${stripped.droppedAssistantMessages} thinking-only assistant turn(s)` : "";
|
|
3396
|
+
consola.warn(`Native /v1/messages signature retry: stripped ${stripped.strippedBlocks} thinking block(s)${droppedSuffix}`);
|
|
3397
|
+
return await dispatchAnthropicRequest(stripped.payload, options$1);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async function dispatchAnthropicRequest(payload, options$1) {
|
|
3401
|
+
if (state.multiAccountEnabled && accountManager.hasAccounts()) return createWithMultiAccount(payload, options$1);
|
|
3402
|
+
return createWithSingleAccount(payload, options$1);
|
|
3403
|
+
}
|
|
3404
|
+
async function createWithSingleAccount(payload, options$1) {
|
|
3405
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
3406
|
+
const url = `${copilotBaseUrl(state)}/v1/messages`;
|
|
3407
|
+
const buildHeaders = () => buildAnthropicHeaders(payload, state, options$1);
|
|
3408
|
+
const bodyString = JSON.stringify(payload);
|
|
3409
|
+
consola.debug("Sending request to Copilot (native Anthropic):", {
|
|
3410
|
+
model: payload.model,
|
|
3411
|
+
endpoint: url,
|
|
3412
|
+
stream: payload.stream
|
|
3413
|
+
});
|
|
3414
|
+
let response = await fetchWithTimeout(url, {
|
|
3415
|
+
method: "POST",
|
|
3416
|
+
headers: buildHeaders(),
|
|
3417
|
+
body: bodyString,
|
|
3418
|
+
signal: options$1?.signal
|
|
3419
|
+
});
|
|
3420
|
+
if (response.status === 401) {
|
|
3421
|
+
consola.warn("Copilot token expired, refreshing and retrying...");
|
|
3422
|
+
try {
|
|
3423
|
+
await refreshCopilotToken();
|
|
3424
|
+
response = await fetchWithTimeout(url, {
|
|
3425
|
+
method: "POST",
|
|
3426
|
+
headers: buildHeaders(),
|
|
3427
|
+
body: bodyString,
|
|
3428
|
+
signal: options$1?.signal
|
|
3429
|
+
});
|
|
3430
|
+
} catch (refreshError) {
|
|
3431
|
+
consola.warn(`Failed to refresh token: ${rootCause(refreshError)}`);
|
|
3432
|
+
consola.debug("Failed to refresh token:", refreshError);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
if (!response.ok) await throwUpstreamError(response);
|
|
3436
|
+
if (payload.stream) {
|
|
3437
|
+
const gen = events(response);
|
|
3438
|
+
gen.__accountInfo = { apiBaseUrl: copilotBaseUrl(state) };
|
|
3439
|
+
return gen;
|
|
3440
|
+
}
|
|
3441
|
+
return await response.json();
|
|
3442
|
+
}
|
|
3443
|
+
function buildTokenSource(account) {
|
|
3444
|
+
return {
|
|
3445
|
+
copilotToken: account.copilotToken,
|
|
3446
|
+
copilotApiEndpoint: account.copilotApiEndpoint,
|
|
3447
|
+
accountType: account.accountType,
|
|
3448
|
+
githubToken: account.githubToken,
|
|
3449
|
+
vsCodeVersion: state.vsCodeVersion,
|
|
3450
|
+
machineId: account.machineId,
|
|
3451
|
+
sessionId: account.sessionId,
|
|
3452
|
+
proxy: account.proxy
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
function tagStreamWithAccount(result, account, source) {
|
|
3456
|
+
if (typeof result === "object" && Symbol.asyncIterator in result) result.__accountInfo = {
|
|
3457
|
+
accountId: account.id,
|
|
3458
|
+
accountProxy: account.proxy,
|
|
3459
|
+
apiBaseUrl: copilotBaseUrl(source)
|
|
3460
|
+
};
|
|
3461
|
+
return result;
|
|
3462
|
+
}
|
|
3463
|
+
async function handleMultiAccount401(ctx, account) {
|
|
3464
|
+
try {
|
|
3465
|
+
await accountManager.refreshAccountToken(account);
|
|
3466
|
+
ctx.source.copilotToken = account.copilotToken;
|
|
3467
|
+
const retried = await doFetchAnthropic(ctx);
|
|
3468
|
+
accountManager.markAccountSuccess(account.id);
|
|
3469
|
+
return tagStreamWithAccount(retried, account, ctx.source);
|
|
3470
|
+
} catch (refreshError) {
|
|
3471
|
+
accountManager.markAccountStatus(account.id, "banned", "GitHub token invalid");
|
|
3472
|
+
throw refreshError;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
async function createWithMultiAccount(payload, options$1) {
|
|
3476
|
+
const triedAccountIds = /* @__PURE__ */ new Set();
|
|
3477
|
+
let lastError;
|
|
3478
|
+
let networkRetried = false;
|
|
3479
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3480
|
+
const account = accountManager.getActiveAccount();
|
|
3481
|
+
if (!account || triedAccountIds.has(account.id)) break;
|
|
3482
|
+
triedAccountIds.add(account.id);
|
|
3483
|
+
if (!account.copilotToken) {
|
|
3484
|
+
consola.debug(`Account ${account.label} has no copilot token, refreshing...`);
|
|
3485
|
+
await accountManager.refreshAccountToken(account);
|
|
3486
|
+
if (!account.copilotToken) {
|
|
3487
|
+
accountManager.markAccountStatus(account.id, "error", "No copilot token");
|
|
3488
|
+
continue;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
const ctx = {
|
|
3492
|
+
payload,
|
|
3493
|
+
source: buildTokenSource(account),
|
|
3494
|
+
accountId: account.id,
|
|
3495
|
+
options: options$1
|
|
3496
|
+
};
|
|
3497
|
+
try {
|
|
3498
|
+
const result = await doFetchAnthropic(ctx);
|
|
3499
|
+
account.lastRequestAt = Date.now();
|
|
3500
|
+
accountManager.markAccountSuccess(account.id);
|
|
3501
|
+
return tagStreamWithAccount(result, account, ctx.source);
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
lastError = error;
|
|
3504
|
+
if (error instanceof HTTPError) {
|
|
3505
|
+
if (error.response.status === 401) return handleMultiAccount401(ctx, account);
|
|
3506
|
+
if (error.response.status >= 400 && error.response.status < 500) throw error;
|
|
3507
|
+
consola.warn(`Account ${account.label}: 5xx from /v1/messages, trying next account`);
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
const errMsg = error.message || String(error);
|
|
3511
|
+
if (!networkRetried) {
|
|
3512
|
+
networkRetried = true;
|
|
3513
|
+
consola.warn(`Account ${account.label}: network error on /v1/messages, resetting pool and retrying once: ${errMsg}`);
|
|
3514
|
+
resetAccountConnections(account.id);
|
|
3515
|
+
triedAccountIds.delete(account.id);
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
consola.warn(`Account ${account.label}: network error after retry on /v1/messages (giving up): ${errMsg}`);
|
|
3519
|
+
throw error;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
if (lastError) throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Network request failed");
|
|
3523
|
+
throw new Error("No available accounts");
|
|
3524
|
+
}
|
|
3525
|
+
async function doFetchAnthropic(ctx) {
|
|
3526
|
+
const { payload, source, accountId, options: options$1 } = ctx;
|
|
3527
|
+
const url = `${copilotBaseUrl(source)}/v1/messages`;
|
|
3528
|
+
const bodyString = JSON.stringify(payload);
|
|
3529
|
+
consola.debug("Sending request to Copilot (multi-account, native Anthropic):", {
|
|
3530
|
+
model: payload.model,
|
|
3531
|
+
endpoint: url,
|
|
3532
|
+
stream: payload.stream
|
|
3533
|
+
});
|
|
3534
|
+
const response = await fetchWithTimeout(url, {
|
|
3535
|
+
method: "POST",
|
|
3536
|
+
headers: buildAnthropicHeaders(payload, source, options$1),
|
|
3537
|
+
body: bodyString,
|
|
3538
|
+
signal: options$1?.signal
|
|
3539
|
+
}, {
|
|
3540
|
+
accountId,
|
|
3541
|
+
accountProxy: source.proxy,
|
|
3542
|
+
externalSignal: options$1?.signal
|
|
3543
|
+
});
|
|
3544
|
+
if (!response.ok) await throwUpstreamError(response);
|
|
3545
|
+
if (payload.stream) return events(response);
|
|
3546
|
+
return await response.json();
|
|
3547
|
+
}
|
|
3548
|
+
async function throwUpstreamError(response) {
|
|
3549
|
+
const errorBody = await response.text();
|
|
3550
|
+
if (response.status === 400) consola.debug(`/v1/messages 400: ${errorBody}`);
|
|
3551
|
+
else consola.error("Failed native Anthropic request", {
|
|
3552
|
+
status: response.status,
|
|
3553
|
+
statusText: response.statusText,
|
|
3554
|
+
body: errorBody
|
|
3555
|
+
});
|
|
3556
|
+
throw new HTTPError(`Failed to call /v1/messages: ${response.status} ${errorBody}`, response);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3160
3559
|
//#endregion
|
|
3161
3560
|
//#region src/routes/messages/stream-translation.ts
|
|
3162
3561
|
function isToolBlockOpen(state$1) {
|
|
@@ -3445,8 +3844,146 @@ async function handleCompletion(c) {
|
|
|
3445
3844
|
messages_count: anthropicPayload.messages.length,
|
|
3446
3845
|
max_tokens: anthropicPayload.max_tokens
|
|
3447
3846
|
});
|
|
3448
|
-
const openAIPayload = translateToOpenAI(stripSystemReminders(anthropicPayload));
|
|
3449
3847
|
if (state.manualApprove) await awaitApproval();
|
|
3848
|
+
const route = resolveAnthropicRoute(anthropicPayload.model);
|
|
3849
|
+
consola.debug(`Anthropic route resolved: ${route}`);
|
|
3850
|
+
if (route === "native-anthropic") return handleNativePassthrough(c, anthropicPayload);
|
|
3851
|
+
return handleTranslatedCompletion(c, anthropicPayload);
|
|
3852
|
+
}
|
|
3853
|
+
async function handleNativePassthrough(c, anthropicPayload) {
|
|
3854
|
+
const anthropicBeta = c.req.header("anthropic-beta");
|
|
3855
|
+
let result;
|
|
3856
|
+
try {
|
|
3857
|
+
result = await createAnthropicMessages(stripSystemReminders(anthropicPayload), { anthropicBeta });
|
|
3858
|
+
} catch (error) {
|
|
3859
|
+
consola.warn(`Native /v1/messages failed: ${error.message || String(error)}`);
|
|
3860
|
+
throw error;
|
|
3861
|
+
}
|
|
3862
|
+
if (!anthropicPayload.stream) return c.json(overrideAnthropicResponseModel(result, anthropicPayload.model));
|
|
3863
|
+
const stream = result;
|
|
3864
|
+
const accountInfo = stream.__accountInfo;
|
|
3865
|
+
const proxied = accountInfo ? isAccountProxied(accountInfo.accountProxy) : isProxyActive();
|
|
3866
|
+
const heartbeatMs = proxied ? HEARTBEAT_PROXIED_MS : HEARTBEAT_DIRECT_MS;
|
|
3867
|
+
const upstreamTimeoutMs = proxied ? UPSTREAM_TIMEOUT_PROXIED_MS : UPSTREAM_TIMEOUT_DIRECT_MS;
|
|
3868
|
+
consola.debug(`Native SSE config: proxied=${proxied}, heartbeat=${heartbeatMs / 1e3}s, timeout=${upstreamTimeoutMs / 1e3}s`);
|
|
3869
|
+
return streamSSE(c, async (sse) => {
|
|
3870
|
+
const abortController = new AbortController();
|
|
3871
|
+
sse.onAbort(() => abortController.abort());
|
|
3872
|
+
try {
|
|
3873
|
+
await consumeNativeStreamWithHeartbeat(stream, sse, {
|
|
3874
|
+
heartbeatMs,
|
|
3875
|
+
upstreamTimeoutMs,
|
|
3876
|
+
abortSignal: abortController.signal,
|
|
3877
|
+
requestedModel: anthropicPayload.model
|
|
3878
|
+
});
|
|
3879
|
+
} catch (error) {
|
|
3880
|
+
if (!abortController.signal.aborted) {
|
|
3881
|
+
const message = error.message || String(error);
|
|
3882
|
+
consola.warn(`Native SSE stream interrupted: ${message}`);
|
|
3883
|
+
resetConnections();
|
|
3884
|
+
await sendErrorEvent(sse);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
/** Resolve the Anthropic event type for a raw SSE frame. */
|
|
3890
|
+
function resolveAnthropicEventType(rawEvent) {
|
|
3891
|
+
if (rawEvent.event) return rawEvent.event;
|
|
3892
|
+
if (!rawEvent.data) return void 0;
|
|
3893
|
+
try {
|
|
3894
|
+
return JSON.parse(rawEvent.data).type;
|
|
3895
|
+
} catch {
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
async function handleNativeHeartbeatTick(stream, silenceMs, upstreamTimeoutMs) {
|
|
3900
|
+
if (silenceMs >= upstreamTimeoutMs) {
|
|
3901
|
+
consola.warn(`Upstream silent for ${Math.round(silenceMs / 1e3)}s (limit ${upstreamTimeoutMs / 1e3}s), closing native stream`);
|
|
3902
|
+
resetConnections();
|
|
3903
|
+
await sendErrorEvent(stream);
|
|
3904
|
+
return { action: "break" };
|
|
3905
|
+
}
|
|
3906
|
+
await stream.writeSSE({
|
|
3907
|
+
event: "ping",
|
|
3908
|
+
data: "{\"type\":\"ping\"}"
|
|
3909
|
+
});
|
|
3910
|
+
return { action: "continue" };
|
|
3911
|
+
}
|
|
3912
|
+
/** Forward one native event; returns true if it was `message_stop`. */
|
|
3913
|
+
async function forwardNativeEvent(stream, rawEvent, requestedModel) {
|
|
3914
|
+
if (rawEvent.data === "[DONE]") return {
|
|
3915
|
+
forwarded: false,
|
|
3916
|
+
isMessageStop: false
|
|
3917
|
+
};
|
|
3918
|
+
if (!rawEvent.data) return {
|
|
3919
|
+
forwarded: true,
|
|
3920
|
+
isMessageStop: false
|
|
3921
|
+
};
|
|
3922
|
+
const eventType = resolveAnthropicEventType(rawEvent);
|
|
3923
|
+
if (!eventType) {
|
|
3924
|
+
consola.debug("Skipping native SSE chunk with no resolvable type");
|
|
3925
|
+
return {
|
|
3926
|
+
forwarded: true,
|
|
3927
|
+
isMessageStop: false
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
3930
|
+
const dataToSend = eventType === "message_start" && requestedModel ? overrideMessageStartEventModel(rawEvent.data, requestedModel) : rawEvent.data;
|
|
3931
|
+
await stream.writeSSE({
|
|
3932
|
+
event: eventType,
|
|
3933
|
+
data: dataToSend
|
|
3934
|
+
});
|
|
3935
|
+
return {
|
|
3936
|
+
forwarded: true,
|
|
3937
|
+
isMessageStop: eventType === "message_stop"
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Consume an upstream Anthropic SSE generator and forward events untouched.
|
|
3942
|
+
* Heartbeat and upstream-timeout logic mirrors the translated path.
|
|
3943
|
+
*/
|
|
3944
|
+
async function consumeNativeStreamWithHeartbeat(response, stream, opts) {
|
|
3945
|
+
const { heartbeatMs, upstreamTimeoutMs, abortSignal, requestedModel } = opts;
|
|
3946
|
+
const iter = response[Symbol.asyncIterator]();
|
|
3947
|
+
let pendingNext = iter.next();
|
|
3948
|
+
let lastDataAt = Date.now();
|
|
3949
|
+
let sawMessageStop = false;
|
|
3950
|
+
try {
|
|
3951
|
+
while (true) {
|
|
3952
|
+
if (abortSignal?.aborted) {
|
|
3953
|
+
consola.debug("Client disconnected, stopping native SSE consumption");
|
|
3954
|
+
break;
|
|
3955
|
+
}
|
|
3956
|
+
const raceResult = await Promise.race([pendingNext.then((r) => ({
|
|
3957
|
+
kind: "data",
|
|
3958
|
+
result: r
|
|
3959
|
+
})), heartbeatDelay(heartbeatMs)]);
|
|
3960
|
+
if (raceResult === HEARTBEAT) {
|
|
3961
|
+
if ((await handleNativeHeartbeatTick(stream, Date.now() - lastDataAt, upstreamTimeoutMs)).action === "break") break;
|
|
3962
|
+
continue;
|
|
3963
|
+
}
|
|
3964
|
+
const { result: iterResult } = raceResult;
|
|
3965
|
+
if (iterResult.done) break;
|
|
3966
|
+
lastDataAt = Date.now();
|
|
3967
|
+
pendingNext = iter.next();
|
|
3968
|
+
const rawEvent = iterResult.value;
|
|
3969
|
+
const result = await forwardNativeEvent(stream, rawEvent, requestedModel);
|
|
3970
|
+
if (!result.forwarded) break;
|
|
3971
|
+
if (result.isMessageStop) sawMessageStop = true;
|
|
3972
|
+
}
|
|
3973
|
+
if (!sawMessageStop && !abortSignal?.aborted) try {
|
|
3974
|
+
await stream.writeSSE({
|
|
3975
|
+
event: "message_stop",
|
|
3976
|
+
data: "{\"type\":\"message_stop\"}"
|
|
3977
|
+
});
|
|
3978
|
+
} catch {}
|
|
3979
|
+
} finally {
|
|
3980
|
+
try {
|
|
3981
|
+
await iter.return(void 0);
|
|
3982
|
+
} catch {}
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
async function handleTranslatedCompletion(c, anthropicPayload) {
|
|
3986
|
+
const openAIPayload = translateToOpenAI(stripSystemReminders(anthropicPayload));
|
|
3450
3987
|
const response = await createChatCompletions(openAIPayload);
|
|
3451
3988
|
if (isNonStreaming(response)) return c.json(translateToAnthropic(response));
|
|
3452
3989
|
const accountInfo = response.__accountInfo;
|
|
@@ -3680,7 +4217,7 @@ async function validateGitHubToken(token) {
|
|
|
3680
4217
|
state.githubToken = token;
|
|
3681
4218
|
consola.info("Using provided GitHub token");
|
|
3682
4219
|
try {
|
|
3683
|
-
const { getGitHubUser } = await import("./get-user-
|
|
4220
|
+
const { getGitHubUser } = await import("./get-user-Brre_x1N.js");
|
|
3684
4221
|
const user = await getGitHubUser();
|
|
3685
4222
|
consola.info(`Logged in as ${user.login}`);
|
|
3686
4223
|
} catch (error) {
|
|
@@ -3719,6 +4256,8 @@ async function runServer(options$1) {
|
|
|
3719
4256
|
state.rateLimitWait = options$1.rateLimitWait;
|
|
3720
4257
|
state.showToken = options$1.showToken;
|
|
3721
4258
|
state.apiKeys = options$1.apiKeys;
|
|
4259
|
+
state.disableAnthropicPassthrough = options$1.disableAnthropicPassthrough;
|
|
4260
|
+
if (options$1.disableAnthropicPassthrough) consola.info("Native Anthropic passthrough DISABLED — all /v1/messages requests will translate via /chat/completions");
|
|
3722
4261
|
if (state.apiKeys && state.apiKeys.length > 0) consola.info(`API key authentication enabled with ${state.apiKeys.length} key(s)`);
|
|
3723
4262
|
await ensurePaths();
|
|
3724
4263
|
await cacheVSCodeVersion();
|
|
@@ -3731,10 +4270,10 @@ async function runServer(options$1) {
|
|
|
3731
4270
|
try {
|
|
3732
4271
|
await setupCopilotToken();
|
|
3733
4272
|
} catch (error) {
|
|
3734
|
-
const { HTTPError: HTTPError$1 } = await import("./error-
|
|
4273
|
+
const { HTTPError: HTTPError$1 } = await import("./error-DO0tIuq0.js");
|
|
3735
4274
|
if (error instanceof HTTPError$1 && error.response.status === 401) {
|
|
3736
4275
|
consola.error("Failed to get Copilot token - GitHub token may be invalid or Copilot access revoked");
|
|
3737
|
-
const { clearGithubToken: clearGithubToken$1 } = await import("./token-
|
|
4276
|
+
const { clearGithubToken: clearGithubToken$1 } = await import("./token-Dv2Iyk1S.js");
|
|
3738
4277
|
await clearGithubToken$1();
|
|
3739
4278
|
consola.info("Please restart to re-authenticate");
|
|
3740
4279
|
}
|
|
@@ -3817,6 +4356,11 @@ const start = defineCommand({
|
|
|
3817
4356
|
"api-key": {
|
|
3818
4357
|
type: "string",
|
|
3819
4358
|
description: "API keys for authentication"
|
|
4359
|
+
},
|
|
4360
|
+
"disable-anthropic-passthrough": {
|
|
4361
|
+
type: "boolean",
|
|
4362
|
+
default: false,
|
|
4363
|
+
description: "Force translate all /v1/messages requests via /chat/completions (disable native Copilot Anthropic endpoint)"
|
|
3820
4364
|
}
|
|
3821
4365
|
},
|
|
3822
4366
|
run({ args }) {
|
|
@@ -3836,7 +4380,8 @@ const start = defineCommand({
|
|
|
3836
4380
|
claudeCode: args["claude-code"],
|
|
3837
4381
|
showToken: args["show-token"],
|
|
3838
4382
|
proxyEnv: args["proxy-env"],
|
|
3839
|
-
apiKeys
|
|
4383
|
+
apiKeys,
|
|
4384
|
+
disableAnthropicPassthrough: args["disable-anthropic-passthrough"]
|
|
3840
4385
|
});
|
|
3841
4386
|
}
|
|
3842
4387
|
});
|