opencode-copilot-account-switcher 0.10.6 → 0.10.8
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/copilot-network-retry.js +14 -10
- package/dist/plugin-hooks.d.ts +11 -0
- package/dist/plugin-hooks.js +115 -16
- package/dist/routing-state.d.ts +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,6 @@ const RETRYABLE_MESSAGES = [
|
|
|
3
3
|
"load failed",
|
|
4
4
|
"failed to fetch",
|
|
5
5
|
"network request failed",
|
|
6
|
-
"sse read timed out",
|
|
7
6
|
"unable to connect",
|
|
8
7
|
"econnreset",
|
|
9
8
|
"etimedout",
|
|
@@ -490,8 +489,9 @@ function stripOpenAIItemId(part) {
|
|
|
490
489
|
};
|
|
491
490
|
}
|
|
492
491
|
function getInternalPatchClient(client) {
|
|
493
|
-
const
|
|
494
|
-
|
|
492
|
+
const internalClient = client?._client;
|
|
493
|
+
const patch = internalClient?.patch;
|
|
494
|
+
return typeof patch === "function" ? patch.bind(internalClient) : undefined;
|
|
495
495
|
}
|
|
496
496
|
function collectSessionRepairMatches(messages, predicate) {
|
|
497
497
|
return (messages?.data ?? []).flatMap((message) => {
|
|
@@ -1077,21 +1077,25 @@ function withStreamDebugLogs(response, request) {
|
|
|
1077
1077
|
}
|
|
1078
1078
|
catch (error) {
|
|
1079
1079
|
const message = getErrorMessage(error);
|
|
1080
|
+
const isSseReadTimeout = message.includes("sse read timed out");
|
|
1080
1081
|
const retryable = RETRYABLE_MESSAGES.some((part) => message.includes(part));
|
|
1081
1082
|
if (isDebugEnabled()) {
|
|
1082
1083
|
debugLog("sse stream read error", {
|
|
1083
1084
|
url: rawUrl,
|
|
1084
1085
|
message,
|
|
1085
1086
|
retryableByMessage: retryable,
|
|
1087
|
+
bypassedTimeoutWrap: isSseReadTimeout,
|
|
1086
1088
|
});
|
|
1087
1089
|
}
|
|
1088
|
-
controller.error(
|
|
1089
|
-
?
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1090
|
+
controller.error(isSseReadTimeout
|
|
1091
|
+
? error
|
|
1092
|
+
: retryable
|
|
1093
|
+
? toRetryableApiCallError(error, request, {
|
|
1094
|
+
group: "stream",
|
|
1095
|
+
statusCode: response.status,
|
|
1096
|
+
responseHeaders: response.headers,
|
|
1097
|
+
})
|
|
1098
|
+
: error);
|
|
1095
1099
|
}
|
|
1096
1100
|
};
|
|
1097
1101
|
void pump();
|
package/dist/plugin-hooks.d.ts
CHANGED
|
@@ -55,7 +55,18 @@ export declare function buildPluginHooks(input: {
|
|
|
55
55
|
loadOfficialConfig?: (input: {
|
|
56
56
|
getAuth: () => Promise<CopilotAuthState | undefined>;
|
|
57
57
|
provider?: CopilotProviderConfig;
|
|
58
|
+
baseFetch?: typeof fetch;
|
|
59
|
+
version?: string;
|
|
58
60
|
}) => Promise<OfficialCopilotConfig | undefined>;
|
|
61
|
+
finalizeRequestForSelection?: (input: {
|
|
62
|
+
request: Request | URL | string;
|
|
63
|
+
init?: RequestInit;
|
|
64
|
+
getAuth: () => Promise<CopilotAuthState | undefined>;
|
|
65
|
+
provider?: CopilotProviderConfig;
|
|
66
|
+
}) => Promise<{
|
|
67
|
+
request: Request | URL | string;
|
|
68
|
+
init?: RequestInit;
|
|
69
|
+
} | undefined>;
|
|
59
70
|
loadOfficialChatHeaders?: (input: {
|
|
60
71
|
client?: object;
|
|
61
72
|
directory?: string;
|
package/dist/plugin-hooks.js
CHANGED
|
@@ -19,6 +19,7 @@ const RATE_LIMIT_COOLDOWN_MS = 10 * 60 * 1000;
|
|
|
19
19
|
const MAX_SESSION_BINDINGS = 256;
|
|
20
20
|
const TOUCH_WRITE_CACHE_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
21
21
|
const MAX_TOUCH_WRITE_CACHE_ENTRIES = 2048;
|
|
22
|
+
const INTERNAL_DEBUG_LINK_HEADER = "x-opencode-debug-link-id";
|
|
22
23
|
export class InjectCommandHandledError extends Error {
|
|
23
24
|
constructor() {
|
|
24
25
|
super("copilot-inject-handled");
|
|
@@ -186,6 +187,7 @@ function getInternalSessionID(request, init) {
|
|
|
186
187
|
function stripInternalSessionHeader(request, init) {
|
|
187
188
|
return mergeAndRewriteRequestHeaders(request, init, (headers) => {
|
|
188
189
|
headers.delete("x-opencode-session-id");
|
|
190
|
+
headers.delete(INTERNAL_DEBUG_LINK_HEADER);
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
193
|
function toLoadMap(value) {
|
|
@@ -223,6 +225,8 @@ function toErrorMessage(error) {
|
|
|
223
225
|
function toConsumptionReasonText(reason) {
|
|
224
226
|
if (reason === "subagent")
|
|
225
227
|
return "子代理请求";
|
|
228
|
+
if (reason === "compaction")
|
|
229
|
+
return "上下文压缩";
|
|
226
230
|
if (reason === "user-reselect")
|
|
227
231
|
return "用户回合重选";
|
|
228
232
|
return "常规请求";
|
|
@@ -239,6 +243,13 @@ function buildConsumptionToast(input) {
|
|
|
239
243
|
variant: "info",
|
|
240
244
|
};
|
|
241
245
|
}
|
|
246
|
+
function shouldShowConsumptionToast(input) {
|
|
247
|
+
if (input.reason === "subagent")
|
|
248
|
+
return input.isFirstUse;
|
|
249
|
+
if (input.reason === "compaction")
|
|
250
|
+
return false;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
242
253
|
function chooseCandidateAccount(input) {
|
|
243
254
|
const lowest = pickLowestWithRandom(input.candidates, input.loads, input.random);
|
|
244
255
|
const boundName = input.sessionBindings.get(input.sessionID)?.accountName;
|
|
@@ -400,6 +411,48 @@ export function buildPluginHooks(input) {
|
|
|
400
411
|
: undefined,
|
|
401
412
|
}];
|
|
402
413
|
};
|
|
414
|
+
const classifyRequestReason = async (requestInput) => {
|
|
415
|
+
const initiator = getMergedRequestHeader(requestInput.request, requestInput.init, "x-initiator");
|
|
416
|
+
if (initiator !== "agent") {
|
|
417
|
+
return {
|
|
418
|
+
reason: toReasonByInitiator(initiator),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const sessionLookup = input.client?.session?.get;
|
|
422
|
+
const messageLookup = input.client?.session?.message;
|
|
423
|
+
const messageIDHeader = getMergedRequestHeader(requestInput.request, requestInput.init, INTERNAL_DEBUG_LINK_HEADER);
|
|
424
|
+
if (typeof messageIDHeader === "string" && messageIDHeader.length > 0) {
|
|
425
|
+
const currentMessage = await messageLookup?.({
|
|
426
|
+
path: {
|
|
427
|
+
id: requestInput.sessionID,
|
|
428
|
+
messageID: messageIDHeader,
|
|
429
|
+
},
|
|
430
|
+
query: {
|
|
431
|
+
directory: input.directory,
|
|
432
|
+
},
|
|
433
|
+
throwOnError: true,
|
|
434
|
+
}).catch(() => undefined);
|
|
435
|
+
const parts = Array.isArray(currentMessage?.data?.parts) ? currentMessage.data.parts : undefined;
|
|
436
|
+
if (parts?.some((part) => part?.type === "compaction") === true) {
|
|
437
|
+
return {
|
|
438
|
+
reason: "compaction",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const session = await sessionLookup?.({
|
|
443
|
+
path: {
|
|
444
|
+
id: requestInput.sessionID,
|
|
445
|
+
},
|
|
446
|
+
query: {
|
|
447
|
+
directory: input.directory,
|
|
448
|
+
},
|
|
449
|
+
throwOnError: true,
|
|
450
|
+
}).catch(() => undefined);
|
|
451
|
+
const isTrueChildSession = typeof session?.data?.parentID === "string" && session.data.parentID.length > 0;
|
|
452
|
+
return {
|
|
453
|
+
reason: isTrueChildSession ? "subagent" : "regular",
|
|
454
|
+
};
|
|
455
|
+
};
|
|
403
456
|
const getLatestLastAccountSwitchAt = async () => {
|
|
404
457
|
const store = readRetryStoreContext(await loadStore().catch(() => undefined));
|
|
405
458
|
return store?.lastAccountSwitchAt;
|
|
@@ -426,12 +479,42 @@ export function buildPluginHooks(input) {
|
|
|
426
479
|
const loader = async (getAuth, provider) => {
|
|
427
480
|
const authOverride = new AsyncLocalStorage();
|
|
428
481
|
const getScopedAuth = async () => authOverride.getStore() ?? getAuth();
|
|
482
|
+
const providerConfig = provider;
|
|
429
483
|
const config = await loadOfficialConfig({
|
|
430
484
|
getAuth: getScopedAuth,
|
|
431
|
-
provider:
|
|
485
|
+
provider: providerConfig,
|
|
432
486
|
});
|
|
433
487
|
if (!config)
|
|
434
488
|
return {};
|
|
489
|
+
const finalizeRequestForSelection = input.finalizeRequestForSelection
|
|
490
|
+
?? (input.loadOfficialConfig
|
|
491
|
+
? undefined
|
|
492
|
+
: async (selectionInput) => {
|
|
493
|
+
let captured;
|
|
494
|
+
const captureConfig = await loadOfficialConfig({
|
|
495
|
+
getAuth: getScopedAuth,
|
|
496
|
+
provider: providerConfig,
|
|
497
|
+
baseFetch: async (nextRequest, nextInit) => {
|
|
498
|
+
captured = {
|
|
499
|
+
request: nextRequest,
|
|
500
|
+
init: nextInit,
|
|
501
|
+
};
|
|
502
|
+
return new Response("{}", {
|
|
503
|
+
status: 200,
|
|
504
|
+
headers: {
|
|
505
|
+
"content-type": "application/json",
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
if (!captureConfig)
|
|
511
|
+
return undefined;
|
|
512
|
+
const inspectionRequest = selectionInput.request instanceof Request
|
|
513
|
+
? selectionInput.request.clone()
|
|
514
|
+
: selectionInput.request;
|
|
515
|
+
await captureConfig.fetch(inspectionRequest, selectionInput.init).catch(() => undefined);
|
|
516
|
+
return captured;
|
|
517
|
+
});
|
|
435
518
|
const store = await loadStore().catch(() => undefined);
|
|
436
519
|
const retryStore = readRetryStoreContext(store);
|
|
437
520
|
const fetchWithModelAccount = async (request, init) => {
|
|
@@ -447,7 +530,15 @@ export function buildPluginHooks(input) {
|
|
|
447
530
|
maxEntries: touchWriteCacheMaxEntries,
|
|
448
531
|
});
|
|
449
532
|
const sessionID = getInternalSessionID(request, init);
|
|
450
|
-
const
|
|
533
|
+
const finalized = await finalizeRequestForSelection?.({
|
|
534
|
+
request,
|
|
535
|
+
init,
|
|
536
|
+
getAuth: getScopedAuth,
|
|
537
|
+
provider: providerConfig,
|
|
538
|
+
}).catch(() => undefined);
|
|
539
|
+
const selectionRequest = finalized?.request ?? request;
|
|
540
|
+
const selectionInit = finalized?.init ?? init;
|
|
541
|
+
const initiator = getMergedRequestHeader(selectionRequest, selectionInit, "x-initiator");
|
|
451
542
|
const allowReselect = initiator === "user";
|
|
452
543
|
const candidates = latestStore ? resolveCopilotModelAccounts(latestStore, modelID) : [];
|
|
453
544
|
const hasExplicitModelGroup = Boolean(latestStore
|
|
@@ -482,8 +573,13 @@ export function buildPluginHooks(input) {
|
|
|
482
573
|
return config.fetch(outbound.request, outbound.init);
|
|
483
574
|
}
|
|
484
575
|
const candidateNames = candidates.map((item) => item.name);
|
|
576
|
+
const classification = sessionID.length > 0
|
|
577
|
+
? await classifyRequestReason({ sessionID, request: selectionRequest, init: selectionInit })
|
|
578
|
+
: {
|
|
579
|
+
reason: toReasonByInitiator(initiator),
|
|
580
|
+
};
|
|
485
581
|
let decisionLoads = loadMapToRecord(loads, candidateNames);
|
|
486
|
-
let decisionReason =
|
|
582
|
+
let decisionReason = classification.reason;
|
|
487
583
|
let decisionSwitched = false;
|
|
488
584
|
let decisionSwitchFrom;
|
|
489
585
|
let decisionSwitchBlockedBy;
|
|
@@ -755,19 +851,21 @@ export function buildPluginHooks(input) {
|
|
|
755
851
|
retryAfterMs: decisionRetryAfterMs,
|
|
756
852
|
},
|
|
757
853
|
}).catch(() => undefined);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
854
|
+
if (shouldShowConsumptionToast({ reason: decisionReason, isFirstUse })) {
|
|
855
|
+
const consumptionToast = buildConsumptionToast({
|
|
856
|
+
accountName: finalChosenAccount,
|
|
857
|
+
reason: decisionReason,
|
|
858
|
+
switchFrom: decisionSwitchFrom,
|
|
859
|
+
});
|
|
860
|
+
await showStatusToast({
|
|
861
|
+
client: input.client,
|
|
862
|
+
message: consumptionToast.message,
|
|
863
|
+
variant: consumptionToast.variant,
|
|
864
|
+
warn: (scope, error) => {
|
|
865
|
+
console.warn(`[${scope}] failed to show toast`, error);
|
|
866
|
+
},
|
|
867
|
+
}).catch(() => undefined);
|
|
868
|
+
}
|
|
771
869
|
return response;
|
|
772
870
|
};
|
|
773
871
|
if (retryStore?.networkRetryEnabled !== true)
|
|
@@ -890,6 +988,7 @@ export function buildPluginHooks(input) {
|
|
|
890
988
|
},
|
|
891
989
|
});
|
|
892
990
|
}
|
|
991
|
+
output.headers[INTERNAL_DEBUG_LINK_HEADER] = hookInput.message.id;
|
|
893
992
|
output.headers["x-opencode-session-id"] = hookInput.sessionID;
|
|
894
993
|
};
|
|
895
994
|
return {
|
package/dist/routing-state.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export type RouteDecisionEvent = {
|
|
|
69
69
|
groupSource: "model" | "active";
|
|
70
70
|
candidateNames: string[];
|
|
71
71
|
loads: Record<string, number>;
|
|
72
|
-
reason: "regular" | "subagent" | "user-reselect" | "rate-limit-switch";
|
|
72
|
+
reason: "regular" | "subagent" | "compaction" | "user-reselect" | "rate-limit-switch";
|
|
73
73
|
switched: boolean;
|
|
74
74
|
switchFrom?: string;
|
|
75
75
|
switchBlockedBy?: "no-cooled-down-candidate" | "replacement-load-higher" | "routing-state-read-failed" | "no-replacement-candidate";
|