@centerforagenticai/pi-multi-account 0.1.4 → 0.1.6
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 +17 -3
- package/package.json +5 -5
- package/packages/pi-anthropic-oauth/package.json +2 -2
- package/packages/pi-anthropic-oauth/src/stream.ts +26 -8
- package/packages/pi-anthropic-oauth/src/transport-activity.ts +59 -0
- package/packages/pi-antigravity/package.json +2 -2
- package/packages/pi-antigravity/src/models/discovery.ts +2 -1
- package/packages/pi-antigravity/src/models/grouping.ts +12 -10
- package/packages/pi-antigravity/src/models/models.ts +11 -4
- package/src/account-group-failure.ts +149 -0
- package/src/account-group-members.ts +139 -0
- package/src/anthropic-adaptive-stream.ts +18 -9
- package/src/anthropic-alias-stream.ts +13 -68
- package/src/codex-adapter.ts +78 -50
- package/src/commands.ts +3 -3
- package/src/config.ts +197 -2
- package/src/diagnostic-store.ts +17 -15
- package/src/diagnostics.ts +97 -35
- package/src/host-final-stop-message.ts +20 -118
- package/src/index.ts +452 -87
- package/src/logical-dispatch.ts +12 -13
- package/src/logical-provider.ts +1282 -355
- package/src/model-fallback-policy.ts +384 -0
- package/src/models-declaration.ts +15 -8
- package/src/public-assistant-projection.ts +163 -0
- package/src/recovery-engine.ts +648 -110
- package/src/recovery-plan.ts +7 -1
- package/src/recovery-send-evidence.ts +29 -0
- package/src/refusal-advice.ts +139 -0
- package/src/routing.ts +4 -8
- package/src/runtime-state.ts +7 -0
- package/src/shared-usage.ts +21 -4
- package/src/upstream-anthropic.ts +12 -4
- package/src/upstream-antigravity.ts +2 -38
- package/src/usage-fetch.ts +42 -59
package/src/index.ts
CHANGED
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
type OperatorAccount,
|
|
20
20
|
} from "./commands.js";
|
|
21
21
|
import { CompactionRouter } from "./compaction.js";
|
|
22
|
+
import {
|
|
23
|
+
accountGroupMemberAvailability,
|
|
24
|
+
isAccountGroupVirtualModel,
|
|
25
|
+
readAccountGroupRegistrySnapshot,
|
|
26
|
+
} from "./account-group-members.js";
|
|
22
27
|
import {
|
|
23
28
|
createCodexAliasProviderConfig,
|
|
24
29
|
getCodexModelsFromRegistry,
|
|
@@ -35,6 +40,8 @@ import {
|
|
|
35
40
|
import {
|
|
36
41
|
ALLOWED_FAMILIES,
|
|
37
42
|
DEFAULT_CONFIG,
|
|
43
|
+
ConfigValidationError,
|
|
44
|
+
isAccountLimit,
|
|
38
45
|
accountSlotIndexes,
|
|
39
46
|
canonicalProviderIdForAccountSlot,
|
|
40
47
|
isAccountSlotIndex,
|
|
@@ -80,6 +87,7 @@ import {
|
|
|
80
87
|
import { DiagnosticStore } from "./diagnostic-store.js";
|
|
81
88
|
import { DeclarationNoticeMarker } from "./declaration-notice-marker.js";
|
|
82
89
|
import { DiagnosticLog } from "./diagnostics.js";
|
|
90
|
+
import { createRefusalAdvisor } from "./refusal-advice.js";
|
|
83
91
|
import {
|
|
84
92
|
classifyProviderId,
|
|
85
93
|
createPublicAuthStorageAdapter,
|
|
@@ -136,6 +144,7 @@ import {
|
|
|
136
144
|
type InstalledDeclarationStatus,
|
|
137
145
|
} from "./models-declaration.js";
|
|
138
146
|
import { createLogicalDispatch } from "./logical-dispatch.js";
|
|
147
|
+
import type { AccountGroupFailureBlockReason } from "./account-group-failure.js";
|
|
139
148
|
import type { ModelsCatalogs } from "./commands.js";
|
|
140
149
|
import {
|
|
141
150
|
NOOP_LOGICAL_ATTRIBUTION_ATTEMPT,
|
|
@@ -155,6 +164,7 @@ import {
|
|
|
155
164
|
import { isCredentialUsable } from "./credential-lifecycle.js";
|
|
156
165
|
import {
|
|
157
166
|
routeAfterFailure,
|
|
167
|
+
logicalAccountEligible,
|
|
158
168
|
selectAvailableManagedAccount,
|
|
159
169
|
selectAvailableRecoveryAccount,
|
|
160
170
|
selectAvailableRouteCandidates,
|
|
@@ -200,7 +210,7 @@ import {
|
|
|
200
210
|
type SessionIdSource,
|
|
201
211
|
} from "./session-account-groups.js";
|
|
202
212
|
import { projectKeyForCwd } from "./project-identity.js";
|
|
203
|
-
import { RuntimeState, isCanonicalManagedProviderId } from "./runtime-state.js";
|
|
213
|
+
import { RuntimeState, isCanonicalManagedProviderId, normalizeCredentialRevision } from "./runtime-state.js";
|
|
204
214
|
import { providerTypeFor } from "./vendor.js";
|
|
205
215
|
import { resolveTierModel } from "./tier-model-resolver.js";
|
|
206
216
|
import {
|
|
@@ -408,6 +418,17 @@ function readInstalledDeclaration(targetPath: string): unknown {
|
|
|
408
418
|
}
|
|
409
419
|
}
|
|
410
420
|
|
|
421
|
+
/** Only schema field names, never validator prose or operator-supplied values. */
|
|
422
|
+
function configRepairMessage(error: unknown): string {
|
|
423
|
+
const field = error instanceof ConfigValidationError
|
|
424
|
+
? [...Object.keys(DEFAULT_CONFIG), "defaultAccountGroup"].find((key) =>
|
|
425
|
+
error.message.startsWith(`[multi-account config] ${key} `) ||
|
|
426
|
+
error.message.startsWith(`[multi-account config] ${key}.`),
|
|
427
|
+
)
|
|
428
|
+
: undefined;
|
|
429
|
+
return `Multi-account configuration is invalid${field === undefined ? "" : ` (${field})`}. Correct the global config and run /multi-account reload or /reload.`;
|
|
430
|
+
}
|
|
431
|
+
|
|
411
432
|
/** The session's id, or undefined when the host context cannot supply one. */
|
|
412
433
|
function readSessionId(ctx: ExtensionContext): string | undefined {
|
|
413
434
|
try {
|
|
@@ -1603,11 +1624,12 @@ function readLiveCredentialFacts(
|
|
|
1603
1624
|
): {
|
|
1604
1625
|
credential: CredentialUsability | undefined;
|
|
1605
1626
|
fingerprint: string | undefined;
|
|
1627
|
+
credentialRevision: number | undefined;
|
|
1606
1628
|
} {
|
|
1607
1629
|
try {
|
|
1608
1630
|
const stored = readStoredCredential(providerId, authPath);
|
|
1609
1631
|
if (stored === null || typeof stored !== "object") {
|
|
1610
|
-
return { credential: undefined, fingerprint: undefined };
|
|
1632
|
+
return { credential: undefined, fingerprint: undefined, credentialRevision: undefined };
|
|
1611
1633
|
}
|
|
1612
1634
|
const record = stored as Record<string, unknown>;
|
|
1613
1635
|
const expiresAtMs =
|
|
@@ -1621,10 +1643,11 @@ function readLiveCredentialFacts(
|
|
|
1621
1643
|
...(expiresAtMs === undefined ? {} : { expiresAtMs }),
|
|
1622
1644
|
},
|
|
1623
1645
|
fingerprint: accountFingerprint(record.access),
|
|
1646
|
+
credentialRevision: expiresAtMs,
|
|
1624
1647
|
};
|
|
1625
1648
|
} catch {
|
|
1626
1649
|
// Missing or unreadable metadata must preserve existing reactive routing.
|
|
1627
|
-
return { credential: undefined, fingerprint: undefined };
|
|
1650
|
+
return { credential: undefined, fingerprint: undefined, credentialRevision: undefined };
|
|
1628
1651
|
}
|
|
1629
1652
|
}
|
|
1630
1653
|
|
|
@@ -1637,7 +1660,7 @@ function readLiveCredentialFacts(
|
|
|
1637
1660
|
* routing keep the managed superset.
|
|
1638
1661
|
*/
|
|
1639
1662
|
type OperatorManagedAccount = Omit<OperatorAccount, "family"> &
|
|
1640
|
-
Omit<ManagedAccount, "family"> & { readonly family: ManagedFamily };
|
|
1663
|
+
Omit<ManagedAccount, "family"> & { readonly family: ManagedFamily; readonly credentialRevision?: number };
|
|
1641
1664
|
|
|
1642
1665
|
export type AccountGroupEnforcementReason =
|
|
1643
1666
|
| "unknown-group"
|
|
@@ -1729,7 +1752,7 @@ function operatorAccounts(
|
|
|
1729
1752
|
// Project the stored credential at this boundary into bounded metadata only.
|
|
1730
1753
|
// The raw access value is used transiently by accountFingerprint and never
|
|
1731
1754
|
// enters OperatorAccount, ManagedAccount, routing state, or diagnostics.
|
|
1732
|
-
const { credential, fingerprint } = readLiveCredentialFacts(
|
|
1755
|
+
const { credential, fingerprint, credentialRevision } = readLiveCredentialFacts(
|
|
1733
1756
|
slot.providerId,
|
|
1734
1757
|
authPath,
|
|
1735
1758
|
);
|
|
@@ -1746,6 +1769,7 @@ function operatorAccounts(
|
|
|
1746
1769
|
modelIds: models.map((candidate) => candidate.id),
|
|
1747
1770
|
displayName: slot.providerId,
|
|
1748
1771
|
...(credential === undefined ? {} : { credential }),
|
|
1772
|
+
...(credentialRevision === undefined ? {} : { credentialRevision }),
|
|
1749
1773
|
...(fingerprint === undefined
|
|
1750
1774
|
? {}
|
|
1751
1775
|
: { accountFingerprint: fingerprint }),
|
|
@@ -1962,9 +1986,10 @@ function delegateOwnedApiSet(): WeakSet<object> {
|
|
|
1962
1986
|
* True when this extension API belongs to a delegate-owned in-process session
|
|
1963
1987
|
* rather than the real foreground originator.
|
|
1964
1988
|
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1989
|
+
* Read at session_start, which bindExtensions() emits inside the trusted frame,
|
|
1990
|
+
* not while the loader executes the factory. An active current bind has a defined
|
|
1991
|
+
* AsyncLocalStorage frame; a legacy bind has positive depth. Either signal marks
|
|
1992
|
+
* the API persistently, matching pi-delegate's own
|
|
1968
1993
|
* shouldSkipForegroundLifecycleForDelegateOwnedApi contract.
|
|
1969
1994
|
*/
|
|
1970
1995
|
function isDelegateOwnedApi(api: Parameters<ExtensionFactory>[0]): boolean {
|
|
@@ -2031,7 +2056,7 @@ function delegateWorkerOriginOwnerSessionId(
|
|
|
2031
2056
|
|
|
2032
2057
|
/**
|
|
2033
2058
|
* Persist an inherited effective result under the child's own session id without
|
|
2034
|
-
* consulting the child's real cwd/default policy. This makes a
|
|
2059
|
+
* consulting the child's real cwd/default policy. This makes a delegate child a
|
|
2035
2060
|
* valid parent for the next durable or in-process hop while preserving the
|
|
2036
2061
|
* inherited source exactly.
|
|
2037
2062
|
*/
|
|
@@ -2117,13 +2142,9 @@ function buildLivePiCatalogSnapshot(
|
|
|
2117
2142
|
export const createMultiAccountExtension =
|
|
2118
2143
|
(options: MultiAccountExtensionOptions = {}): ExtensionFactory =>
|
|
2119
2144
|
async (pi) => {
|
|
2120
|
-
//
|
|
2121
|
-
//
|
|
2122
|
-
|
|
2123
|
-
// same qualified widget key and overwrite the real foreground footer. The
|
|
2124
|
-
// footer is foreground-only; every other surface (provider registration,
|
|
2125
|
-
// routing, cost attribution) stays active for the worker.
|
|
2126
|
-
const delegateOwnedSession = isDelegateOwnedApi(pi);
|
|
2145
|
+
// Ownership becomes available at session_start, not factory loading. Keep
|
|
2146
|
+
// the bind-time result for later routing and foreground-only UI callbacks.
|
|
2147
|
+
let delegateOwnedSession = false;
|
|
2127
2148
|
// Copy only pi-delegate's two named child-identity fields. Public request
|
|
2128
2149
|
// objects and caller-provided env maps never cross this trust boundary.
|
|
2129
2150
|
const delegateDriverChildMarker = process.env[DELEGATE_DRIVER_CHILD_ENV];
|
|
@@ -2260,6 +2281,17 @@ export const createMultiAccountExtension =
|
|
|
2260
2281
|
let latestLogicalPhysicalProviderId: string | undefined;
|
|
2261
2282
|
const logicalTerminalAssociations = createLogicalTerminalAssociationStore();
|
|
2262
2283
|
let config: MultiAccountConfig = DEFAULT_CONFIG;
|
|
2284
|
+
// Discovery defaults are not request authorization. Cleared only by a
|
|
2285
|
+
// complete validated load; a failed reload keeps the last valid policy.
|
|
2286
|
+
let policyConfigError: string | undefined =
|
|
2287
|
+
"Multi-account configuration is not loaded. Run /multi-account reload or /reload.";
|
|
2288
|
+
const loadGlobalConfig = (): MultiAccountConfig => {
|
|
2289
|
+
try {
|
|
2290
|
+
return readConfig(configPath);
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
throw new Error(configRepairMessage(error));
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2263
2295
|
const defaultCostReportReader = createDefaultCostReportReader({
|
|
2264
2296
|
config: () => config,
|
|
2265
2297
|
// `context` is undefined until `session_start` assigns it below; this
|
|
@@ -2398,11 +2430,23 @@ export const createMultiAccountExtension =
|
|
|
2398
2430
|
return executeLogicalModelSwitch(args, dependencies);
|
|
2399
2431
|
};
|
|
2400
2432
|
|
|
2401
|
-
const openRouterPolicy = () =>
|
|
2402
|
-
resolveOpenRouterEnvironmentPolicy({
|
|
2433
|
+
const openRouterPolicy = () => {
|
|
2434
|
+
const policy = resolveOpenRouterEnvironmentPolicy({
|
|
2403
2435
|
environment: openRouterEnvironment,
|
|
2404
2436
|
sessionDisabled: openRouterSessionDisabled,
|
|
2405
2437
|
});
|
|
2438
|
+
// In-process workers share the parent's environment, so lineage env
|
|
2439
|
+
// alone cannot exclude them. Only retained trusted bind ownership
|
|
2440
|
+
// supplies this additional veto; caller-shaped origins never do.
|
|
2441
|
+
return delegateOwnedSession
|
|
2442
|
+
? {
|
|
2443
|
+
enabled: false as const,
|
|
2444
|
+
reason: "delegate-session" as const,
|
|
2445
|
+
conversationEgressConsented: policy.conversationEgressConsented,
|
|
2446
|
+
delegatesAllowed: false as const,
|
|
2447
|
+
}
|
|
2448
|
+
: policy;
|
|
2449
|
+
};
|
|
2406
2450
|
const openRouterModel = (
|
|
2407
2451
|
modelId: string,
|
|
2408
2452
|
modelContext: ExtensionContext | undefined = context,
|
|
@@ -2468,7 +2512,9 @@ export const createMultiAccountExtension =
|
|
|
2468
2512
|
| undefined;
|
|
2469
2513
|
let accountGroupResolutionInitialized = false;
|
|
2470
2514
|
let accountGroupScopeBlocked = false;
|
|
2471
|
-
|
|
2515
|
+
let accountGroupBlockReason: AccountGroupFailureBlockReason | undefined;
|
|
2516
|
+
const blockAccountGroupScope = (key: AccountGroupFailureBlockReason, message: string): void => {
|
|
2517
|
+
accountGroupBlockReason = key;
|
|
2472
2518
|
accountGroupResolutionInitialized = true;
|
|
2473
2519
|
accountGroupScopeBlocked = true;
|
|
2474
2520
|
effectiveAccountGroupResolution = undefined;
|
|
@@ -2479,10 +2525,12 @@ export const createMultiAccountExtension =
|
|
|
2479
2525
|
): EffectiveAccountGroupResolution => {
|
|
2480
2526
|
accountGroupResolutionInitialized = true;
|
|
2481
2527
|
accountGroupScopeBlocked = false;
|
|
2528
|
+
accountGroupBlockReason = undefined;
|
|
2482
2529
|
effectiveAccountGroupResolution = resolution;
|
|
2483
2530
|
return resolution;
|
|
2484
2531
|
};
|
|
2485
2532
|
const accountGroupScopeRestrictsRouting = (): boolean =>
|
|
2533
|
+
policyConfigError !== undefined ||
|
|
2486
2534
|
accountGroupScopeBlocked ||
|
|
2487
2535
|
effectiveAccountGroupResolution === undefined ||
|
|
2488
2536
|
effectiveAccountGroupResolution.source !== "unrestricted";
|
|
@@ -2500,6 +2548,10 @@ export const createMultiAccountExtension =
|
|
|
2500
2548
|
const initializeSessionAccountGroupScope = (
|
|
2501
2549
|
accountContext: ExtensionContext | undefined,
|
|
2502
2550
|
): void => {
|
|
2551
|
+
if (policyConfigError !== undefined) {
|
|
2552
|
+
blockAccountGroupScope("config-invalid", policyConfigError);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2503
2555
|
const sessionManager = accountContext?.sessionManager as
|
|
2504
2556
|
| AccountGroupSessionManager
|
|
2505
2557
|
| undefined;
|
|
@@ -2546,14 +2598,23 @@ export const createMultiAccountExtension =
|
|
|
2546
2598
|
);
|
|
2547
2599
|
return;
|
|
2548
2600
|
}
|
|
2549
|
-
const
|
|
2550
|
-
sessionAccountGroups.
|
|
2551
|
-
if (
|
|
2552
|
-
|
|
2601
|
+
const inherited =
|
|
2602
|
+
sessionAccountGroups.readCachedResolution(parentSessionId);
|
|
2603
|
+
if (inherited === undefined) {
|
|
2604
|
+
blockAccountGroupScope(
|
|
2605
|
+
"delegate-resolution-unavailable",
|
|
2606
|
+
"The delegate parent's effective account group is unavailable; managed routing is blocked for this worker.",
|
|
2607
|
+
);
|
|
2553
2608
|
return;
|
|
2554
2609
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2610
|
+
rememberAccountGroupResolution(
|
|
2611
|
+
cacheInheritedAccountGroupResolution(
|
|
2612
|
+
sessionAccountGroups,
|
|
2613
|
+
sessionManager,
|
|
2614
|
+
cwd,
|
|
2615
|
+
inherited,
|
|
2616
|
+
),
|
|
2617
|
+
);
|
|
2557
2618
|
return;
|
|
2558
2619
|
}
|
|
2559
2620
|
if (delegateDriverIdentityPresent) {
|
|
@@ -2600,6 +2661,7 @@ export const createMultiAccountExtension =
|
|
|
2600
2661
|
accounts: readonly OperatorManagedAccount[],
|
|
2601
2662
|
accountContext: ExtensionContext | undefined,
|
|
2602
2663
|
): OperatorManagedAccount[] => {
|
|
2664
|
+
if (policyConfigError !== undefined) return [];
|
|
2603
2665
|
if (!accountGroupResolutionInitialized) {
|
|
2604
2666
|
initializeSessionAccountGroupScope(accountContext);
|
|
2605
2667
|
}
|
|
@@ -2648,13 +2710,17 @@ export const createMultiAccountExtension =
|
|
|
2648
2710
|
(account): account is OperatorAccount & SubscriptionManagedAccount =>
|
|
2649
2711
|
isRoutingEligibleAccountFamily(account),
|
|
2650
2712
|
);
|
|
2651
|
-
const physicalAccountsForRouting = (
|
|
2652
|
-
|
|
2713
|
+
const physicalAccountsForRouting = (
|
|
2714
|
+
nowMs: number,
|
|
2715
|
+
accounts: readonly OperatorManagedAccount[] = physicalOperatorAccounts(),
|
|
2716
|
+
): ManagedAccount[] =>
|
|
2717
|
+
accounts.map((account) => {
|
|
2653
2718
|
const {
|
|
2654
2719
|
providerId,
|
|
2655
2720
|
family,
|
|
2656
2721
|
credentialType,
|
|
2657
2722
|
credential,
|
|
2723
|
+
credentialRevision,
|
|
2658
2724
|
accountFingerprint,
|
|
2659
2725
|
modelIds,
|
|
2660
2726
|
} = account;
|
|
@@ -2681,6 +2747,7 @@ export const createMultiAccountExtension =
|
|
|
2681
2747
|
: { accountFingerprint }),
|
|
2682
2748
|
...(modelIds === undefined ? {} : { modelIds }),
|
|
2683
2749
|
...(fleetUsage === undefined ? {} : { fleetUsage }),
|
|
2750
|
+
...(account.credentialRevision === undefined ? {} : { credentialRevision: account.credentialRevision }),
|
|
2684
2751
|
};
|
|
2685
2752
|
});
|
|
2686
2753
|
const subscriptionAccountsForRouting = (
|
|
@@ -2898,6 +2965,10 @@ export const createMultiAccountExtension =
|
|
|
2898
2965
|
authJsonPath: authPath,
|
|
2899
2966
|
config,
|
|
2900
2967
|
});
|
|
2968
|
+
for (const slot of discovered.slots) {
|
|
2969
|
+
const revision = normalizeCredentialRevision(slot.expiresAtMs ?? NaN);
|
|
2970
|
+
if (revision !== undefined) state.observeCredentialRevision(slot.providerId, slot.family, revision);
|
|
2971
|
+
}
|
|
2901
2972
|
// When codex is disabled (host OAuth surface unavailable), strip codex
|
|
2902
2973
|
// slots so the disabled sentinel's throwing callbacks are never reached.
|
|
2903
2974
|
discovery = codexEnabled
|
|
@@ -3116,44 +3187,58 @@ export const createMultiAccountExtension =
|
|
|
3116
3187
|
};
|
|
3117
3188
|
};
|
|
3118
3189
|
|
|
3119
|
-
const
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
) {
|
|
3125
|
-
throw new Error("The live session identity is unavailable.");
|
|
3190
|
+
const readCommandAccountGroupStatus = (): AccountGroupCommandStatus => {
|
|
3191
|
+
if (policyConfigError !== undefined) throw new Error(policyConfigError);
|
|
3192
|
+
const resolution = effectiveAccountGroupResolution;
|
|
3193
|
+
if (accountGroupScopeBlocked || resolution === undefined) {
|
|
3194
|
+
throw new Error("The effective session account group is unavailable; managed routing is blocked.");
|
|
3126
3195
|
}
|
|
3127
|
-
const resolution = resolveOwnSessionAccountGroup(
|
|
3128
|
-
liveContext.sessionManager,
|
|
3129
|
-
liveContext.cwd,
|
|
3130
|
-
);
|
|
3131
3196
|
if (resolution.source === "unrestricted") return { resolution };
|
|
3132
3197
|
const members = config.accountGroups?.[resolution.groupId] ?? [];
|
|
3133
3198
|
const nowMs = Date.now();
|
|
3134
|
-
const
|
|
3199
|
+
const snapshot = readAccountGroupRegistrySnapshot(context?.modelRegistry);
|
|
3200
|
+
const providerIds = new Set([...members, ...snapshot.providerIds]);
|
|
3201
|
+
providerIds.delete(LOGICAL_PROVIDER_ID);
|
|
3135
3202
|
return {
|
|
3136
3203
|
resolution,
|
|
3137
|
-
members:
|
|
3138
|
-
const
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3204
|
+
members: [...providerIds].map((providerId) => {
|
|
3205
|
+
const availability = accountGroupMemberAvailability(providerId, members, snapshot);
|
|
3206
|
+
if (!availability.eligible) return availability;
|
|
3207
|
+
if (providerId === OPENROUTER_PROVIDER_ID) {
|
|
3208
|
+
return { providerId, eligible: false, reason: "metered OpenRouter disabled by active group" };
|
|
3209
|
+
}
|
|
3210
|
+
const slot = classifyProviderId({ providerId, credentialType: "unknown" });
|
|
3211
|
+
if (slot === null) return availability;
|
|
3212
|
+
const fleetUsage = isAllowedFamily(slot.family)
|
|
3213
|
+
? sharedUsageHint(
|
|
3214
|
+
usage.routingUsage(providerId, slot.family, nowMs),
|
|
3215
|
+
usage.activeExhaustionHoldUntilMs(providerId, slot.family, nowMs),
|
|
3216
|
+
)
|
|
3217
|
+
: undefined;
|
|
3218
|
+
const eligible = !disabledProviders.has(providerId) && logicalAccountEligible(
|
|
3219
|
+
{ providerId, exhausted: snapshotIndicatesExhaustion(fleetUsage, nowMs, "all-observed") },
|
|
3220
|
+
state, nowMs,
|
|
3221
|
+
);
|
|
3222
|
+
return { providerId, eligible, reason: eligible ? availability.reason : "not currently routing-eligible" };
|
|
3153
3223
|
}),
|
|
3154
3224
|
};
|
|
3155
3225
|
};
|
|
3156
3226
|
|
|
3227
|
+
// Explicit operator actions resolve this session's own policy. Observational
|
|
3228
|
+
// status must instead preserve the effective result inherited at startup.
|
|
3229
|
+
const resolveCommandAccountGroup = (): AccountGroupCommandStatus => {
|
|
3230
|
+
if (policyConfigError !== undefined) throw new Error(policyConfigError);
|
|
3231
|
+
const liveContext = context;
|
|
3232
|
+
if (
|
|
3233
|
+
liveContext === undefined ||
|
|
3234
|
+
typeof liveContext.sessionManager?.getSessionId !== "function"
|
|
3235
|
+
) {
|
|
3236
|
+
throw new Error("The live session identity is unavailable.");
|
|
3237
|
+
}
|
|
3238
|
+
resolveOwnSessionAccountGroup(liveContext.sessionManager, liveContext.cwd);
|
|
3239
|
+
return readCommandAccountGroupStatus();
|
|
3240
|
+
};
|
|
3241
|
+
|
|
3157
3242
|
const commands = new MultiAccountCommandController({
|
|
3158
3243
|
state,
|
|
3159
3244
|
usage,
|
|
@@ -3197,7 +3282,7 @@ export const createMultiAccountExtension =
|
|
|
3197
3282
|
sessionAccountGroups.clearOverride(sessionManager);
|
|
3198
3283
|
return resolveCommandAccountGroup();
|
|
3199
3284
|
},
|
|
3200
|
-
status:
|
|
3285
|
+
status: readCommandAccountGroupStatus,
|
|
3201
3286
|
},
|
|
3202
3287
|
logicalRoutingState: () => logicalRoutingState,
|
|
3203
3288
|
disabledProviders,
|
|
@@ -3282,9 +3367,12 @@ export const createMultiAccountExtension =
|
|
|
3282
3367
|
setModel,
|
|
3283
3368
|
rediscover,
|
|
3284
3369
|
addSlot,
|
|
3285
|
-
reloadGlobalConfig: async () =>
|
|
3370
|
+
reloadGlobalConfig: async () => loadGlobalConfig(),
|
|
3286
3371
|
onConfigReload: async (nextConfig) => {
|
|
3287
3372
|
config = nextConfig;
|
|
3373
|
+
policyConfigError = undefined;
|
|
3374
|
+
accountGroupResolutionInitialized = false;
|
|
3375
|
+
initializeSessionAccountGroupScope(context);
|
|
3288
3376
|
await rediscover();
|
|
3289
3377
|
},
|
|
3290
3378
|
routingConfig: {
|
|
@@ -3332,8 +3420,8 @@ export const createMultiAccountExtension =
|
|
|
3332
3420
|
// Group scope is stricter than the optional metered rung. For unrestricted
|
|
3333
3421
|
// sessions, consent, delegate exclusion, session disablement, model syntax,
|
|
3334
3422
|
// and the positive budget cap remain the authority for OpenRouter.
|
|
3335
|
-
// A named
|
|
3336
|
-
//
|
|
3423
|
+
// A named group remains closed to metered OpenRouter even when listed.
|
|
3424
|
+
// Membership alone cannot enable or widen the existing metered gate.
|
|
3337
3425
|
if (accountGroupScopeRestrictsRouting()) return false;
|
|
3338
3426
|
const policy = openRouterPolicy();
|
|
3339
3427
|
if (!policy.enabled) return false;
|
|
@@ -3475,12 +3563,31 @@ export const createMultiAccountExtension =
|
|
|
3475
3563
|
providerId: string,
|
|
3476
3564
|
inputContext: ExtensionContext,
|
|
3477
3565
|
): boolean => {
|
|
3566
|
+
if (policyConfigError !== undefined) return false;
|
|
3478
3567
|
if (!accountGroupScopeRestrictsRouting()) return true;
|
|
3568
|
+
// Pi virtual routers may choose arbitrary physical targets. No public
|
|
3569
|
+
// cross-host hook here can authorize those targets before they send.
|
|
3570
|
+
// Check the selected model before the extension-owned unified exception.
|
|
3571
|
+
if (isAccountGroupVirtualModel(inputContext.model ?? currentModel)) return false;
|
|
3479
3572
|
if (providerId === LOGICAL_PROVIDER_ID) return true;
|
|
3573
|
+
// Listing OpenRouter does not enable a metered route. Issue 138 owns
|
|
3574
|
+
// changing that gate; all existing consent/budget/delegate controls stay.
|
|
3575
|
+
if (providerId === OPENROUTER_PROVIDER_ID) return false;
|
|
3576
|
+
const resolution = effectiveAccountGroupResolution;
|
|
3577
|
+
if (accountGroupScopeBlocked || resolution === undefined || resolution.source === "unrestricted") {
|
|
3578
|
+
return false;
|
|
3579
|
+
}
|
|
3580
|
+
const members = config.accountGroups?.[resolution.groupId] ?? [];
|
|
3581
|
+
const modelId = inputContext.model?.id ?? currentModel?.id;
|
|
3582
|
+
const availability = accountGroupMemberAvailability(
|
|
3583
|
+
providerId, members,
|
|
3584
|
+
readAccountGroupRegistrySnapshot(inputContext.modelRegistry), modelId,
|
|
3585
|
+
);
|
|
3586
|
+
if (!availability.eligible) return false;
|
|
3587
|
+
// Membership never enrolls custom providers in extension-owned routing.
|
|
3588
|
+
if (classifyProviderId({ providerId, credentialType: "unknown" }) === null) return true;
|
|
3480
3589
|
try {
|
|
3481
|
-
return
|
|
3482
|
-
(account) => account.providerId === providerId,
|
|
3483
|
-
);
|
|
3590
|
+
return physicalOperatorAccounts(context ?? inputContext).some((account) => account.providerId === providerId);
|
|
3484
3591
|
} catch {
|
|
3485
3592
|
return false;
|
|
3486
3593
|
}
|
|
@@ -3525,12 +3632,29 @@ export const createMultiAccountExtension =
|
|
|
3525
3632
|
): Promise<{ readonly action: "continue" | "handled" }> => {
|
|
3526
3633
|
const activeProviderId =
|
|
3527
3634
|
inputContext.model?.provider ?? currentModel?.provider;
|
|
3635
|
+
if (policyConfigError !== undefined && activeProviderId === undefined) {
|
|
3636
|
+
inputContext.ui.notify(policyConfigError, "warning");
|
|
3637
|
+
return { action: "handled" };
|
|
3638
|
+
}
|
|
3528
3639
|
if (
|
|
3529
3640
|
activeProviderId === undefined ||
|
|
3530
3641
|
providerAllowedByActiveGroup(activeProviderId, inputContext)
|
|
3531
3642
|
) {
|
|
3532
3643
|
return { action: "continue" };
|
|
3533
3644
|
}
|
|
3645
|
+
if (policyConfigError !== undefined) {
|
|
3646
|
+
inputContext.ui.notify(policyConfigError, "warning");
|
|
3647
|
+
return { action: "handled" };
|
|
3648
|
+
}
|
|
3649
|
+
if (isAccountGroupVirtualModel(inputContext.model ?? currentModel)) {
|
|
3650
|
+
// Never reinterpret a virtual selection as a managed physical origin
|
|
3651
|
+
// or replace it silently, even under a managed provider's name.
|
|
3652
|
+
inputContext.ui.notify(
|
|
3653
|
+
"Multi-account blocked a virtual model under the active account group. Select a physical model allowed by the group.",
|
|
3654
|
+
"warning",
|
|
3655
|
+
);
|
|
3656
|
+
return { action: "handled" };
|
|
3657
|
+
}
|
|
3534
3658
|
const origin = accountGroupOrigin(
|
|
3535
3659
|
activeProviderId,
|
|
3536
3660
|
inputContext.model?.id ?? currentModel?.id,
|
|
@@ -3573,7 +3697,7 @@ export const createMultiAccountExtension =
|
|
|
3573
3697
|
replacement = undefined;
|
|
3574
3698
|
}
|
|
3575
3699
|
}
|
|
3576
|
-
if (replacement !== undefined) {
|
|
3700
|
+
if (replacement !== undefined && !isAccountGroupVirtualModel(replacement)) {
|
|
3577
3701
|
try {
|
|
3578
3702
|
if (await setModel(replacement)) {
|
|
3579
3703
|
if (origin !== undefined) {
|
|
@@ -3780,6 +3904,11 @@ export const createMultiAccountExtension =
|
|
|
3780
3904
|
*/
|
|
3781
3905
|
const settleTurn = async (): Promise<void> => {
|
|
3782
3906
|
openRouterInputApproved = false;
|
|
3907
|
+
if (policyConfigError !== undefined) {
|
|
3908
|
+
pendingFailure = undefined;
|
|
3909
|
+
pendingOpenRouterFailure = false;
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3783
3912
|
if (pendingOpenRouterFailure) {
|
|
3784
3913
|
pendingOpenRouterFailure = false;
|
|
3785
3914
|
const restored = await restoreOpenRouterOrigin();
|
|
@@ -3799,6 +3928,24 @@ export const createMultiAccountExtension =
|
|
|
3799
3928
|
const origin = turnRouteOrigin;
|
|
3800
3929
|
if (!classified || origin === undefined) return;
|
|
3801
3930
|
const { providerId, family, failure } = classified;
|
|
3931
|
+
if (origin.logical) {
|
|
3932
|
+
// Unified recovers inside the model call, so its final failure already
|
|
3933
|
+
// spent the call's bounded sends, and message_end recorded its quota
|
|
3934
|
+
// effects. Settlement adds only a detached usage refresh for a quota
|
|
3935
|
+
// failure and a terminal-auth invalidation: never a switch, park,
|
|
3936
|
+
// OpenRouter rung, route pin, or fixed continuation.
|
|
3937
|
+
// Direct physical selections keep the legacy path below.
|
|
3938
|
+
clearTurnRouteOrigin();
|
|
3939
|
+
if (classified.failedAtMs !== undefined) {
|
|
3940
|
+
refreshUsageAfterLogicalQuotaFailure(
|
|
3941
|
+
providerId,
|
|
3942
|
+
family,
|
|
3943
|
+
classified.failedAtMs,
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
await invalidateOnTerminalAuth(providerId, family, failure);
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3802
3949
|
const accounts = physicalOperatorAccounts();
|
|
3803
3950
|
const nowMs = Date.now();
|
|
3804
3951
|
const routingAccounts = physicalAccountsForRouting(nowMs);
|
|
@@ -4159,6 +4306,15 @@ export const createMultiAccountExtension =
|
|
|
4159
4306
|
const lastFailure = new Map<string, ProviderFailureSignal>();
|
|
4160
4307
|
const handledFailures = new WeakSet<object>();
|
|
4161
4308
|
const observedMessages = new WeakSet<object>();
|
|
4309
|
+
// Operator advice for a structured refusal. A delegate-owned in-process
|
|
4310
|
+
// session shares the foreground UI, so only the foreground advises.
|
|
4311
|
+
const refusalAdvisor = createRefusalAdvisor({
|
|
4312
|
+
// The advisor is constructed before binding; read retained ownership
|
|
4313
|
+
// when advising rather than freezing the factory-time foreground value.
|
|
4314
|
+
get foreground() {
|
|
4315
|
+
return !delegateOwnedSession;
|
|
4316
|
+
},
|
|
4317
|
+
});
|
|
4162
4318
|
const recordUsage = (
|
|
4163
4319
|
observation: () => UsageObservation | undefined,
|
|
4164
4320
|
): void => {
|
|
@@ -4308,8 +4464,9 @@ export const createMultiAccountExtension =
|
|
|
4308
4464
|
const activeProviderId =
|
|
4309
4465
|
runContext.model?.provider ?? currentModel?.provider;
|
|
4310
4466
|
if (
|
|
4311
|
-
activeProviderId !== undefined
|
|
4312
|
-
|
|
4467
|
+
(activeProviderId === undefined && policyConfigError !== undefined) ||
|
|
4468
|
+
(activeProviderId !== undefined &&
|
|
4469
|
+
!providerAllowedByActiveGroup(activeProviderId, runContext))
|
|
4313
4470
|
) {
|
|
4314
4471
|
openRouterInputApproved = false;
|
|
4315
4472
|
diagnostics.record(
|
|
@@ -4318,11 +4475,11 @@ export const createMultiAccountExtension =
|
|
|
4318
4475
|
"A custom-message run selected a provider outside the active account group and was aborted before provider dispatch.",
|
|
4319
4476
|
{ providerId: activeProviderId },
|
|
4320
4477
|
);
|
|
4478
|
+
runContext.abort();
|
|
4321
4479
|
runContext.ui.notify(
|
|
4322
|
-
"Multi-account aborted a provider run outside the active account group.",
|
|
4480
|
+
policyConfigError ?? "Multi-account aborted a provider run outside the active account group.",
|
|
4323
4481
|
"warning",
|
|
4324
4482
|
);
|
|
4325
|
-
runContext.abort();
|
|
4326
4483
|
return;
|
|
4327
4484
|
}
|
|
4328
4485
|
const ownsActiveOpenRouter =
|
|
@@ -4351,6 +4508,10 @@ export const createMultiAccountExtension =
|
|
|
4351
4508
|
pi.on(
|
|
4352
4509
|
"session_start",
|
|
4353
4510
|
guarded("lifecycle.session-start", async (_event, startupContext) => {
|
|
4511
|
+
// Pi executes factories during resource loading, before pi-delegate
|
|
4512
|
+
// opens its trusted bind frame. Capture ownership here before any
|
|
4513
|
+
// UI or routing initialization, and retain it after the frame ends.
|
|
4514
|
+
delegateOwnedSession ||= isDelegateOwnedApi(pi);
|
|
4354
4515
|
context = startupContext;
|
|
4355
4516
|
try {
|
|
4356
4517
|
logicalRouteIndicator?.shutdown();
|
|
@@ -4410,7 +4571,27 @@ export const createMultiAccountExtension =
|
|
|
4410
4571
|
now: Date.now,
|
|
4411
4572
|
});
|
|
4412
4573
|
currentModel = startupContext.model;
|
|
4413
|
-
|
|
4574
|
+
try {
|
|
4575
|
+
config = loadGlobalConfig();
|
|
4576
|
+
policyConfigError = undefined;
|
|
4577
|
+
} catch (error) {
|
|
4578
|
+
// loadGlobalConfig produces only bounded, field-only guidance.
|
|
4579
|
+
const message = (error as Error).message;
|
|
4580
|
+
if (policyConfigError !== undefined) {
|
|
4581
|
+
policyConfigError = message;
|
|
4582
|
+
const raw = readInstalledDeclaration(configPath) as
|
|
4583
|
+
| { accountLimit?: unknown }
|
|
4584
|
+
| undefined;
|
|
4585
|
+
config = {
|
|
4586
|
+
...DEFAULT_CONFIG,
|
|
4587
|
+
accountLimit: isAccountLimit(raw?.accountLimit)
|
|
4588
|
+
? raw.accountLimit
|
|
4589
|
+
: DEFAULT_CONFIG.accountLimit,
|
|
4590
|
+
};
|
|
4591
|
+
}
|
|
4592
|
+
diagnostics.record("warning", "config.invalid", message);
|
|
4593
|
+
startupContext.ui.notify(message, "warning");
|
|
4594
|
+
}
|
|
4414
4595
|
// Resolve and durably cache the session policy before rediscovery can
|
|
4415
4596
|
// publish any request-capable provider surface.
|
|
4416
4597
|
accountGroupResolutionInitialized = false;
|
|
@@ -4575,10 +4756,32 @@ export const createMultiAccountExtension =
|
|
|
4575
4756
|
: decorateLogicalAttributionLifecycle(store, indicator);
|
|
4576
4757
|
},
|
|
4577
4758
|
deps: {
|
|
4759
|
+
captureSelectionSnapshot(modelId) {
|
|
4760
|
+
if (policyConfigError !== undefined) throw new Error(policyConfigError);
|
|
4761
|
+
if (!accountGroupResolutionInitialized) initializeSessionAccountGroupScope(context);
|
|
4762
|
+
const resolution = effectiveAccountGroupResolution;
|
|
4763
|
+
const members = resolution === undefined || resolution.source === "unrestricted" ? [] : [...(config.accountGroups?.[resolution.groupId] ?? [])];
|
|
4764
|
+
const policy = resolution === undefined || accountGroupScopeBlocked || policyConfigError !== undefined
|
|
4765
|
+
? { kind: "blocked" as const, reason: policyConfigError !== undefined ? "config-invalid" as const : accountGroupBlockReason ?? "context-unavailable" }
|
|
4766
|
+
: { kind: "resolved" as const, resolution: resolution.source === "unrestricted" ? { source: "unrestricted" as const } : { source: resolution.source, groupId: resolution.groupId }, members };
|
|
4767
|
+
const accountLimit = config.accountLimit;
|
|
4768
|
+
const nowMs = Date.now();
|
|
4769
|
+
const rows = operatorAccounts(context, discovery, authPath, accountLimit).filter((account) => !disabledProviders.has(account.providerId));
|
|
4770
|
+
const allAccounts = logicalAccountsFromManaged(physicalAccountsForRouting(nowMs, rows), nowMs);
|
|
4771
|
+
const authorized = new Set(enforceSessionAccountGroup(rows, context).map((row) => row.providerId));
|
|
4772
|
+
const registry = readAccountGroupRegistrySnapshot(context?.modelRegistry);
|
|
4773
|
+
const physicalModels = context?.modelRegistry.getAll().filter((row) => !isAccountGroupVirtualModel(row)) ?? [];
|
|
4774
|
+
const otherCandidates = [...registry.providerIds].filter((providerId) => providerId !== LOGICAL_PROVIDER_ID && providerId !== OPENROUTER_PROVIDER_ID && classifyProviderId({ providerId, credentialType: "unknown" }) === null).map((providerId) => {
|
|
4775
|
+
const availability = accountGroupMemberAvailability(providerId, [providerId], registry, modelId);
|
|
4776
|
+
return { providerId, eligible: availability.eligible, reason: availability.reason, servesModel: physicalModels.some((row) => row.provider === providerId && row.id === modelId) };
|
|
4777
|
+
});
|
|
4778
|
+
return { accounts: allAccounts.filter((row) => authorized.has(row.providerId)), group: { policy, accountLimit, allAccounts, otherCandidates } };
|
|
4779
|
+
},
|
|
4578
4780
|
// A getter, not an array. `clear`, `disable`, expiry and
|
|
4579
4781
|
// provider-reported exhaustion must take effect on the next
|
|
4580
4782
|
// request, not at the next restart.
|
|
4581
4783
|
get accounts() {
|
|
4784
|
+
if (policyConfigError !== undefined) throw new Error(policyConfigError);
|
|
4582
4785
|
const nowMs = Date.now();
|
|
4583
4786
|
return logicalAccountsFromManaged(
|
|
4584
4787
|
physicalAccountsForRouting(nowMs),
|
|
@@ -4588,16 +4791,27 @@ export const createMultiAccountExtension =
|
|
|
4588
4791
|
dispatch: createLogicalDispatch(context.modelRegistry),
|
|
4589
4792
|
onPublicTerminal: logicalTerminalAssociations.bindPublicTerminal,
|
|
4590
4793
|
state,
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4794
|
+
// A recovered-past attempt never reaches message_end; commit its
|
|
4795
|
+
// account effects through its own exact association.
|
|
4796
|
+
onSupersededTerminal: (physical: AssistantMessage) => {
|
|
4797
|
+
commitSupersededLogicalFailure(physical);
|
|
4798
|
+
},
|
|
4799
|
+
get recoveryTiming() {
|
|
4800
|
+
return {
|
|
4801
|
+
recoveryIdleTimeoutMs: config.recoveryIdleTimeoutMs,
|
|
4802
|
+
recoveryAbsoluteTimeoutMs: config.recoveryAbsoluteTimeoutMs,
|
|
4803
|
+
recoveryStallTimeoutMs: config.recoveryStallTimeoutMs,
|
|
4804
|
+
};
|
|
4596
4805
|
},
|
|
4597
4806
|
modelVendor,
|
|
4598
4807
|
get tierModelMap() {
|
|
4599
4808
|
return config.tierModelMap;
|
|
4600
4809
|
},
|
|
4810
|
+
// A getter, so a reload that turns same-family failover off
|
|
4811
|
+
// stops the next call's in-call account switch.
|
|
4812
|
+
get sameFamilyFailover() {
|
|
4813
|
+
return config.sameFamilyFailover;
|
|
4814
|
+
},
|
|
4601
4815
|
...(config.crossFamilyChainEnabled
|
|
4602
4816
|
? { crossFamilyChains: config.crossFamilyChains }
|
|
4603
4817
|
: {}),
|
|
@@ -4660,20 +4874,22 @@ export const createMultiAccountExtension =
|
|
|
4660
4874
|
// a fault in the logical gate must not take them down with it.
|
|
4661
4875
|
diagnostics.recordError("logical.registration", error);
|
|
4662
4876
|
}
|
|
4663
|
-
|
|
4664
|
-
.
|
|
4665
|
-
|
|
4666
|
-
(
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4877
|
+
if (policyConfigError === undefined) {
|
|
4878
|
+
void Promise.resolve()
|
|
4879
|
+
.then(() =>
|
|
4880
|
+
warmer.start(
|
|
4881
|
+
() =>
|
|
4882
|
+
projectAutomaticAccountCandidates(
|
|
4883
|
+
discovery,
|
|
4884
|
+
config.accountLimit,
|
|
4885
|
+
),
|
|
4886
|
+
config,
|
|
4887
|
+
),
|
|
4888
|
+
)
|
|
4889
|
+
.catch((error) => {
|
|
4890
|
+
diagnostics.recordError("credential.warmer.start", error);
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4677
4893
|
// Pi restores the session model inside createAgentSession, strictly
|
|
4678
4894
|
// BEFORE extensions load, so a managed alias does not exist yet and the
|
|
4679
4895
|
// host falls back to a base provider. Now that the aliases are
|
|
@@ -4740,6 +4956,7 @@ export const createMultiAccountExtension =
|
|
|
4740
4956
|
guarded(
|
|
4741
4957
|
"lifecycle.before-agent-start",
|
|
4742
4958
|
async (_event, upcomingContext) => {
|
|
4959
|
+
if (policyConfigError !== undefined) return;
|
|
4743
4960
|
// The warmer and usage fetcher each project independently, so
|
|
4744
4961
|
// removing one call-site guard cannot expose the sibling sink.
|
|
4745
4962
|
void Promise.resolve()
|
|
@@ -4775,6 +4992,10 @@ export const createMultiAccountExtension =
|
|
|
4775
4992
|
const accounts = subscriptionOperatorAccounts(
|
|
4776
4993
|
context ?? upcomingContext,
|
|
4777
4994
|
);
|
|
4995
|
+
for (const account of accounts) {
|
|
4996
|
+
const revision = normalizeCredentialRevision(account.credentialRevision ?? NaN);
|
|
4997
|
+
if (revision !== undefined) state.observeCredentialRevision(account.providerId, account.family, revision);
|
|
4998
|
+
}
|
|
4778
4999
|
const nowMs = Date.now();
|
|
4779
5000
|
const managedAccounts: SubscriptionManagedAccount[] = accounts.map(
|
|
4780
5001
|
({
|
|
@@ -4991,6 +5212,137 @@ export const createMultiAccountExtension =
|
|
|
4991
5212
|
});
|
|
4992
5213
|
}),
|
|
4993
5214
|
);
|
|
5215
|
+
/**
|
|
5216
|
+
* A unified terminal-auth failure: invalidate the account, unless an
|
|
5217
|
+
* explicit 401 on a subscription account is repaired by the one forced
|
|
5218
|
+
* refresh the session allows it. The refresh only restores the account for
|
|
5219
|
+
* later calls; it never sends this call again.
|
|
5220
|
+
*/
|
|
5221
|
+
const invalidateOnTerminalAuth = async (
|
|
5222
|
+
providerId: string,
|
|
5223
|
+
family: ManagedFamily,
|
|
5224
|
+
failure: ProviderFailureSignal,
|
|
5225
|
+
): Promise<void> => {
|
|
5226
|
+
// An explicit 401 gets the one forced refresh whatever else the body
|
|
5227
|
+
// says, as direct settlement does: a server-revoked token can still
|
|
5228
|
+
// look locally valid.
|
|
5229
|
+
if (
|
|
5230
|
+
failure.httpStatus === 401 &&
|
|
5231
|
+
forcedCredentialRefresher !== undefined &&
|
|
5232
|
+
isAllowedFamily(family)
|
|
5233
|
+
) {
|
|
5234
|
+
const outcome = await forcedCredentialRefresher
|
|
5235
|
+
.attempt(providerId, family)
|
|
5236
|
+
.catch(() => "failed" as const);
|
|
5237
|
+
if (outcome === "refreshed") return;
|
|
5238
|
+
}
|
|
5239
|
+
if (classifyFailure(failure).category !== "terminal-auth") return;
|
|
5240
|
+
state.invalidateAccount({
|
|
5241
|
+
providerId,
|
|
5242
|
+
family,
|
|
5243
|
+
reason: "terminal-auth-failure",
|
|
5244
|
+
invalidatedAtMs: Date.now(),
|
|
5245
|
+
});
|
|
5246
|
+
};
|
|
5247
|
+
/**
|
|
5248
|
+
* One opportunistic usage refresh for the physical account a unified
|
|
5249
|
+
* quota failure refused on, as direct settlement does for its own.
|
|
5250
|
+
*
|
|
5251
|
+
* Detached and fail-soft: it improves routing state and never delays or
|
|
5252
|
+
* changes the call. Only named fields reach the fetcher, and only for a
|
|
5253
|
+
* live subscription account; an owning-vendor API account has no pooled
|
|
5254
|
+
* usage to refresh.
|
|
5255
|
+
*/
|
|
5256
|
+
const refreshUsageAfterLogicalQuotaFailure = (
|
|
5257
|
+
providerId: string,
|
|
5258
|
+
family: ManagedFamily,
|
|
5259
|
+
failedAtMs: number,
|
|
5260
|
+
): void => {
|
|
5261
|
+
try {
|
|
5262
|
+
const account = physicalOperatorAccounts().find(
|
|
5263
|
+
(candidate) =>
|
|
5264
|
+
candidate.providerId === providerId && candidate.family === family,
|
|
5265
|
+
);
|
|
5266
|
+
if (account === undefined || !isRoutingEligibleAccountFamily(account)) {
|
|
5267
|
+
return;
|
|
5268
|
+
}
|
|
5269
|
+
void usageFetcher
|
|
5270
|
+
.refreshAfterFailure(
|
|
5271
|
+
{
|
|
5272
|
+
providerId: account.providerId,
|
|
5273
|
+
family: account.family,
|
|
5274
|
+
...(account.credentialType === undefined
|
|
5275
|
+
? {}
|
|
5276
|
+
: { credentialType: account.credentialType }),
|
|
5277
|
+
},
|
|
5278
|
+
config,
|
|
5279
|
+
failedAtMs,
|
|
5280
|
+
)
|
|
5281
|
+
.catch(() => undefined);
|
|
5282
|
+
} catch {
|
|
5283
|
+
// A refresh is an improvement, never a step the call depends on.
|
|
5284
|
+
}
|
|
5285
|
+
};
|
|
5286
|
+
/**
|
|
5287
|
+
* Account effects of one unified physical attempt the call recovered past.
|
|
5288
|
+
*
|
|
5289
|
+
* It never reaches message_end, so this applies what message_end and
|
|
5290
|
+
* settlement would: the quota limit streak and, when the provider gave no
|
|
5291
|
+
* recovery time, a bounded exhaustion hold, and one detached usage
|
|
5292
|
+
* refresh; or a terminal-auth invalidation. Its cooldown was already
|
|
5293
|
+
* written before the engine chose the next send. Nothing here switches the
|
|
5294
|
+
* model, continues, parks, or pins.
|
|
5295
|
+
*/
|
|
5296
|
+
const applyLogicalFailureEffects = (
|
|
5297
|
+
association: {
|
|
5298
|
+
readonly route: LogicalRouteFact;
|
|
5299
|
+
readonly response?: ProviderFailureSignal;
|
|
5300
|
+
readonly failure?: ProviderFailureSignal;
|
|
5301
|
+
},
|
|
5302
|
+
dispatchedModelId: string,
|
|
5303
|
+
): void => {
|
|
5304
|
+
const route = association.route;
|
|
5305
|
+
const providerId = route.providerId;
|
|
5306
|
+
const failure: ProviderFailureSignal = {
|
|
5307
|
+
...(association.failure ?? {}),
|
|
5308
|
+
...(association.response ?? {}),
|
|
5309
|
+
modelId: dispatchedModelId,
|
|
5310
|
+
};
|
|
5311
|
+
if (classifyFailure(failure).category === "quota-rate-limit") {
|
|
5312
|
+
const failedAtMs = Date.now();
|
|
5313
|
+
state.recordLimitError(providerId, failedAtMs);
|
|
5314
|
+
// Only the subscription families have shared usage state; an
|
|
5315
|
+
// owning-vendor API account is billed per request, not pooled.
|
|
5316
|
+
if (isAllowedFamily(route.family) && refusalWithoutRecoveryTime(failure)) {
|
|
5317
|
+
usage.recordExhaustionHold(providerId, route.family, failedAtMs);
|
|
5318
|
+
}
|
|
5319
|
+
refreshUsageAfterLogicalQuotaFailure(providerId, route.family, failedAtMs);
|
|
5320
|
+
}
|
|
5321
|
+
void invalidateOnTerminalAuth(providerId, route.family, failure).catch(() => undefined);
|
|
5322
|
+
};
|
|
5323
|
+
const commitSupersededLogicalFailure = (physical: AssistantMessage): void => {
|
|
5324
|
+
const association = logicalTerminalAssociations.consume(physical);
|
|
5325
|
+
let accepted = false;
|
|
5326
|
+
try {
|
|
5327
|
+
if (
|
|
5328
|
+
association !== undefined &&
|
|
5329
|
+
association.outcome === "fail" &&
|
|
5330
|
+
association.dispatchedModelId !== undefined &&
|
|
5331
|
+
boundedPhysicalIdentity(association.dispatchedModelId)
|
|
5332
|
+
) {
|
|
5333
|
+
applyLogicalFailureEffects(association, association.dispatchedModelId);
|
|
5334
|
+
accepted = true;
|
|
5335
|
+
}
|
|
5336
|
+
} catch {
|
|
5337
|
+
diagnostics.record(
|
|
5338
|
+
"warning",
|
|
5339
|
+
"logical.attribution",
|
|
5340
|
+
"A recovered unified attempt's account effects could not be applied.",
|
|
5341
|
+
);
|
|
5342
|
+
} finally {
|
|
5343
|
+
logicalTerminalAssociations.complete(association, accepted);
|
|
5344
|
+
}
|
|
5345
|
+
};
|
|
4994
5346
|
// Observation and classification only. Every switch, park, or resume waits
|
|
4995
5347
|
// for `agent_settled`; see `settleTurn` above.
|
|
4996
5348
|
pi.on("message_end", async (event, messageContext) => {
|
|
@@ -5153,6 +5505,17 @@ export const createMultiAccountExtension =
|
|
|
5153
5505
|
acceptedLogicalAssociation,
|
|
5154
5506
|
);
|
|
5155
5507
|
}
|
|
5508
|
+
// Advice only: names the physical route that refused, never routes.
|
|
5509
|
+
refusalAdvisor.advise(
|
|
5510
|
+
originalMessage,
|
|
5511
|
+
messageContext,
|
|
5512
|
+
acceptedLogicalAssociation && logicalAssociation !== undefined
|
|
5513
|
+
? {
|
|
5514
|
+
providerId: logicalAssociation.route.providerId,
|
|
5515
|
+
modelId: logicalAssociation.dispatchedModelId,
|
|
5516
|
+
}
|
|
5517
|
+
: {},
|
|
5518
|
+
);
|
|
5156
5519
|
|
|
5157
5520
|
try {
|
|
5158
5521
|
const validOriginalMessage = originalMessage as AssistantMessage;
|
|
@@ -5240,6 +5603,8 @@ export const createMultiAccountExtension =
|
|
|
5240
5603
|
) {
|
|
5241
5604
|
return;
|
|
5242
5605
|
}
|
|
5606
|
+
// Advice only: a managed account's structured refusal never routes.
|
|
5607
|
+
refusalAdvisor.advise(event.message, messageContext, identity);
|
|
5243
5608
|
const subscriptionFamily = isRoutingEligibleAccountFamily(messageSlot)
|
|
5244
5609
|
? messageSlot.family
|
|
5245
5610
|
: undefined;
|