@centerforagenticai/pi-multi-account 0.1.5 → 0.1.7
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 +18 -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 +5 -51
- package/src/commands.ts +3 -3
- package/src/config.ts +46 -2
- package/src/diagnostic-store.ts +17 -15
- package/src/diagnostics.ts +97 -35
- package/src/host-final-stop-message.ts +18 -128
- package/src/index.ts +436 -88
- package/src/logical-dispatch.ts +12 -13
- package/src/logical-provider.ts +1239 -407
- package/src/models-declaration.ts +15 -8
- package/src/public-assistant-projection.ts +163 -0
- package/src/recovery-engine.ts +311 -59
- package/src/routing.ts +4 -8
- package/src/runtime-state.ts +7 -0
- package/src/upstream-anthropic.ts +12 -4
- package/src/upstream-antigravity.ts +2 -38
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,
|
|
@@ -137,6 +144,7 @@ import {
|
|
|
137
144
|
type InstalledDeclarationStatus,
|
|
138
145
|
} from "./models-declaration.js";
|
|
139
146
|
import { createLogicalDispatch } from "./logical-dispatch.js";
|
|
147
|
+
import type { AccountGroupFailureBlockReason } from "./account-group-failure.js";
|
|
140
148
|
import type { ModelsCatalogs } from "./commands.js";
|
|
141
149
|
import {
|
|
142
150
|
NOOP_LOGICAL_ATTRIBUTION_ATTEMPT,
|
|
@@ -156,6 +164,7 @@ import {
|
|
|
156
164
|
import { isCredentialUsable } from "./credential-lifecycle.js";
|
|
157
165
|
import {
|
|
158
166
|
routeAfterFailure,
|
|
167
|
+
logicalAccountEligible,
|
|
159
168
|
selectAvailableManagedAccount,
|
|
160
169
|
selectAvailableRecoveryAccount,
|
|
161
170
|
selectAvailableRouteCandidates,
|
|
@@ -201,7 +210,7 @@ import {
|
|
|
201
210
|
type SessionIdSource,
|
|
202
211
|
} from "./session-account-groups.js";
|
|
203
212
|
import { projectKeyForCwd } from "./project-identity.js";
|
|
204
|
-
import { RuntimeState, isCanonicalManagedProviderId } from "./runtime-state.js";
|
|
213
|
+
import { RuntimeState, isCanonicalManagedProviderId, normalizeCredentialRevision } from "./runtime-state.js";
|
|
205
214
|
import { providerTypeFor } from "./vendor.js";
|
|
206
215
|
import { resolveTierModel } from "./tier-model-resolver.js";
|
|
207
216
|
import {
|
|
@@ -409,6 +418,17 @@ function readInstalledDeclaration(targetPath: string): unknown {
|
|
|
409
418
|
}
|
|
410
419
|
}
|
|
411
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
|
+
|
|
412
432
|
/** The session's id, or undefined when the host context cannot supply one. */
|
|
413
433
|
function readSessionId(ctx: ExtensionContext): string | undefined {
|
|
414
434
|
try {
|
|
@@ -1604,11 +1624,12 @@ function readLiveCredentialFacts(
|
|
|
1604
1624
|
): {
|
|
1605
1625
|
credential: CredentialUsability | undefined;
|
|
1606
1626
|
fingerprint: string | undefined;
|
|
1627
|
+
credentialRevision: number | undefined;
|
|
1607
1628
|
} {
|
|
1608
1629
|
try {
|
|
1609
1630
|
const stored = readStoredCredential(providerId, authPath);
|
|
1610
1631
|
if (stored === null || typeof stored !== "object") {
|
|
1611
|
-
return { credential: undefined, fingerprint: undefined };
|
|
1632
|
+
return { credential: undefined, fingerprint: undefined, credentialRevision: undefined };
|
|
1612
1633
|
}
|
|
1613
1634
|
const record = stored as Record<string, unknown>;
|
|
1614
1635
|
const expiresAtMs =
|
|
@@ -1622,10 +1643,11 @@ function readLiveCredentialFacts(
|
|
|
1622
1643
|
...(expiresAtMs === undefined ? {} : { expiresAtMs }),
|
|
1623
1644
|
},
|
|
1624
1645
|
fingerprint: accountFingerprint(record.access),
|
|
1646
|
+
credentialRevision: expiresAtMs,
|
|
1625
1647
|
};
|
|
1626
1648
|
} catch {
|
|
1627
1649
|
// Missing or unreadable metadata must preserve existing reactive routing.
|
|
1628
|
-
return { credential: undefined, fingerprint: undefined };
|
|
1650
|
+
return { credential: undefined, fingerprint: undefined, credentialRevision: undefined };
|
|
1629
1651
|
}
|
|
1630
1652
|
}
|
|
1631
1653
|
|
|
@@ -1638,7 +1660,7 @@ function readLiveCredentialFacts(
|
|
|
1638
1660
|
* routing keep the managed superset.
|
|
1639
1661
|
*/
|
|
1640
1662
|
type OperatorManagedAccount = Omit<OperatorAccount, "family"> &
|
|
1641
|
-
Omit<ManagedAccount, "family"> & { readonly family: ManagedFamily };
|
|
1663
|
+
Omit<ManagedAccount, "family"> & { readonly family: ManagedFamily; readonly credentialRevision?: number };
|
|
1642
1664
|
|
|
1643
1665
|
export type AccountGroupEnforcementReason =
|
|
1644
1666
|
| "unknown-group"
|
|
@@ -1730,7 +1752,7 @@ function operatorAccounts(
|
|
|
1730
1752
|
// Project the stored credential at this boundary into bounded metadata only.
|
|
1731
1753
|
// The raw access value is used transiently by accountFingerprint and never
|
|
1732
1754
|
// enters OperatorAccount, ManagedAccount, routing state, or diagnostics.
|
|
1733
|
-
const { credential, fingerprint } = readLiveCredentialFacts(
|
|
1755
|
+
const { credential, fingerprint, credentialRevision } = readLiveCredentialFacts(
|
|
1734
1756
|
slot.providerId,
|
|
1735
1757
|
authPath,
|
|
1736
1758
|
);
|
|
@@ -1747,6 +1769,7 @@ function operatorAccounts(
|
|
|
1747
1769
|
modelIds: models.map((candidate) => candidate.id),
|
|
1748
1770
|
displayName: slot.providerId,
|
|
1749
1771
|
...(credential === undefined ? {} : { credential }),
|
|
1772
|
+
...(credentialRevision === undefined ? {} : { credentialRevision }),
|
|
1750
1773
|
...(fingerprint === undefined
|
|
1751
1774
|
? {}
|
|
1752
1775
|
: { accountFingerprint: fingerprint }),
|
|
@@ -1963,9 +1986,10 @@ function delegateOwnedApiSet(): WeakSet<object> {
|
|
|
1963
1986
|
* True when this extension API belongs to a delegate-owned in-process session
|
|
1964
1987
|
* rather than the real foreground originator.
|
|
1965
1988
|
*
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
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
|
|
1969
1993
|
* shouldSkipForegroundLifecycleForDelegateOwnedApi contract.
|
|
1970
1994
|
*/
|
|
1971
1995
|
function isDelegateOwnedApi(api: Parameters<ExtensionFactory>[0]): boolean {
|
|
@@ -2032,7 +2056,7 @@ function delegateWorkerOriginOwnerSessionId(
|
|
|
2032
2056
|
|
|
2033
2057
|
/**
|
|
2034
2058
|
* Persist an inherited effective result under the child's own session id without
|
|
2035
|
-
* 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
|
|
2036
2060
|
* valid parent for the next durable or in-process hop while preserving the
|
|
2037
2061
|
* inherited source exactly.
|
|
2038
2062
|
*/
|
|
@@ -2118,13 +2142,9 @@ function buildLivePiCatalogSnapshot(
|
|
|
2118
2142
|
export const createMultiAccountExtension =
|
|
2119
2143
|
(options: MultiAccountExtensionOptions = {}): ExtensionFactory =>
|
|
2120
2144
|
async (pi) => {
|
|
2121
|
-
//
|
|
2122
|
-
//
|
|
2123
|
-
|
|
2124
|
-
// same qualified widget key and overwrite the real foreground footer. The
|
|
2125
|
-
// footer is foreground-only; every other surface (provider registration,
|
|
2126
|
-
// routing, cost attribution) stays active for the worker.
|
|
2127
|
-
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;
|
|
2128
2148
|
// Copy only pi-delegate's two named child-identity fields. Public request
|
|
2129
2149
|
// objects and caller-provided env maps never cross this trust boundary.
|
|
2130
2150
|
const delegateDriverChildMarker = process.env[DELEGATE_DRIVER_CHILD_ENV];
|
|
@@ -2261,6 +2281,17 @@ export const createMultiAccountExtension =
|
|
|
2261
2281
|
let latestLogicalPhysicalProviderId: string | undefined;
|
|
2262
2282
|
const logicalTerminalAssociations = createLogicalTerminalAssociationStore();
|
|
2263
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
|
+
};
|
|
2264
2295
|
const defaultCostReportReader = createDefaultCostReportReader({
|
|
2265
2296
|
config: () => config,
|
|
2266
2297
|
// `context` is undefined until `session_start` assigns it below; this
|
|
@@ -2399,11 +2430,23 @@ export const createMultiAccountExtension =
|
|
|
2399
2430
|
return executeLogicalModelSwitch(args, dependencies);
|
|
2400
2431
|
};
|
|
2401
2432
|
|
|
2402
|
-
const openRouterPolicy = () =>
|
|
2403
|
-
resolveOpenRouterEnvironmentPolicy({
|
|
2433
|
+
const openRouterPolicy = () => {
|
|
2434
|
+
const policy = resolveOpenRouterEnvironmentPolicy({
|
|
2404
2435
|
environment: openRouterEnvironment,
|
|
2405
2436
|
sessionDisabled: openRouterSessionDisabled,
|
|
2406
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
|
+
};
|
|
2407
2450
|
const openRouterModel = (
|
|
2408
2451
|
modelId: string,
|
|
2409
2452
|
modelContext: ExtensionContext | undefined = context,
|
|
@@ -2469,7 +2512,9 @@ export const createMultiAccountExtension =
|
|
|
2469
2512
|
| undefined;
|
|
2470
2513
|
let accountGroupResolutionInitialized = false;
|
|
2471
2514
|
let accountGroupScopeBlocked = false;
|
|
2472
|
-
|
|
2515
|
+
let accountGroupBlockReason: AccountGroupFailureBlockReason | undefined;
|
|
2516
|
+
const blockAccountGroupScope = (key: AccountGroupFailureBlockReason, message: string): void => {
|
|
2517
|
+
accountGroupBlockReason = key;
|
|
2473
2518
|
accountGroupResolutionInitialized = true;
|
|
2474
2519
|
accountGroupScopeBlocked = true;
|
|
2475
2520
|
effectiveAccountGroupResolution = undefined;
|
|
@@ -2480,10 +2525,12 @@ export const createMultiAccountExtension =
|
|
|
2480
2525
|
): EffectiveAccountGroupResolution => {
|
|
2481
2526
|
accountGroupResolutionInitialized = true;
|
|
2482
2527
|
accountGroupScopeBlocked = false;
|
|
2528
|
+
accountGroupBlockReason = undefined;
|
|
2483
2529
|
effectiveAccountGroupResolution = resolution;
|
|
2484
2530
|
return resolution;
|
|
2485
2531
|
};
|
|
2486
2532
|
const accountGroupScopeRestrictsRouting = (): boolean =>
|
|
2533
|
+
policyConfigError !== undefined ||
|
|
2487
2534
|
accountGroupScopeBlocked ||
|
|
2488
2535
|
effectiveAccountGroupResolution === undefined ||
|
|
2489
2536
|
effectiveAccountGroupResolution.source !== "unrestricted";
|
|
@@ -2501,6 +2548,10 @@ export const createMultiAccountExtension =
|
|
|
2501
2548
|
const initializeSessionAccountGroupScope = (
|
|
2502
2549
|
accountContext: ExtensionContext | undefined,
|
|
2503
2550
|
): void => {
|
|
2551
|
+
if (policyConfigError !== undefined) {
|
|
2552
|
+
blockAccountGroupScope("config-invalid", policyConfigError);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2504
2555
|
const sessionManager = accountContext?.sessionManager as
|
|
2505
2556
|
| AccountGroupSessionManager
|
|
2506
2557
|
| undefined;
|
|
@@ -2547,14 +2598,23 @@ export const createMultiAccountExtension =
|
|
|
2547
2598
|
);
|
|
2548
2599
|
return;
|
|
2549
2600
|
}
|
|
2550
|
-
const
|
|
2551
|
-
sessionAccountGroups.
|
|
2552
|
-
if (
|
|
2553
|
-
|
|
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
|
+
);
|
|
2554
2608
|
return;
|
|
2555
2609
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2610
|
+
rememberAccountGroupResolution(
|
|
2611
|
+
cacheInheritedAccountGroupResolution(
|
|
2612
|
+
sessionAccountGroups,
|
|
2613
|
+
sessionManager,
|
|
2614
|
+
cwd,
|
|
2615
|
+
inherited,
|
|
2616
|
+
),
|
|
2617
|
+
);
|
|
2558
2618
|
return;
|
|
2559
2619
|
}
|
|
2560
2620
|
if (delegateDriverIdentityPresent) {
|
|
@@ -2601,6 +2661,7 @@ export const createMultiAccountExtension =
|
|
|
2601
2661
|
accounts: readonly OperatorManagedAccount[],
|
|
2602
2662
|
accountContext: ExtensionContext | undefined,
|
|
2603
2663
|
): OperatorManagedAccount[] => {
|
|
2664
|
+
if (policyConfigError !== undefined) return [];
|
|
2604
2665
|
if (!accountGroupResolutionInitialized) {
|
|
2605
2666
|
initializeSessionAccountGroupScope(accountContext);
|
|
2606
2667
|
}
|
|
@@ -2649,13 +2710,17 @@ export const createMultiAccountExtension =
|
|
|
2649
2710
|
(account): account is OperatorAccount & SubscriptionManagedAccount =>
|
|
2650
2711
|
isRoutingEligibleAccountFamily(account),
|
|
2651
2712
|
);
|
|
2652
|
-
const physicalAccountsForRouting = (
|
|
2653
|
-
|
|
2713
|
+
const physicalAccountsForRouting = (
|
|
2714
|
+
nowMs: number,
|
|
2715
|
+
accounts: readonly OperatorManagedAccount[] = physicalOperatorAccounts(),
|
|
2716
|
+
): ManagedAccount[] =>
|
|
2717
|
+
accounts.map((account) => {
|
|
2654
2718
|
const {
|
|
2655
2719
|
providerId,
|
|
2656
2720
|
family,
|
|
2657
2721
|
credentialType,
|
|
2658
2722
|
credential,
|
|
2723
|
+
credentialRevision,
|
|
2659
2724
|
accountFingerprint,
|
|
2660
2725
|
modelIds,
|
|
2661
2726
|
} = account;
|
|
@@ -2682,6 +2747,7 @@ export const createMultiAccountExtension =
|
|
|
2682
2747
|
: { accountFingerprint }),
|
|
2683
2748
|
...(modelIds === undefined ? {} : { modelIds }),
|
|
2684
2749
|
...(fleetUsage === undefined ? {} : { fleetUsage }),
|
|
2750
|
+
...(account.credentialRevision === undefined ? {} : { credentialRevision: account.credentialRevision }),
|
|
2685
2751
|
};
|
|
2686
2752
|
});
|
|
2687
2753
|
const subscriptionAccountsForRouting = (
|
|
@@ -2899,6 +2965,10 @@ export const createMultiAccountExtension =
|
|
|
2899
2965
|
authJsonPath: authPath,
|
|
2900
2966
|
config,
|
|
2901
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
|
+
}
|
|
2902
2972
|
// When codex is disabled (host OAuth surface unavailable), strip codex
|
|
2903
2973
|
// slots so the disabled sentinel's throwing callbacks are never reached.
|
|
2904
2974
|
discovery = codexEnabled
|
|
@@ -3117,44 +3187,58 @@ export const createMultiAccountExtension =
|
|
|
3117
3187
|
};
|
|
3118
3188
|
};
|
|
3119
3189
|
|
|
3120
|
-
const
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
) {
|
|
3126
|
-
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.");
|
|
3127
3195
|
}
|
|
3128
|
-
const resolution = resolveOwnSessionAccountGroup(
|
|
3129
|
-
liveContext.sessionManager,
|
|
3130
|
-
liveContext.cwd,
|
|
3131
|
-
);
|
|
3132
3196
|
if (resolution.source === "unrestricted") return { resolution };
|
|
3133
3197
|
const members = config.accountGroups?.[resolution.groupId] ?? [];
|
|
3134
3198
|
const nowMs = Date.now();
|
|
3135
|
-
const
|
|
3199
|
+
const snapshot = readAccountGroupRegistrySnapshot(context?.modelRegistry);
|
|
3200
|
+
const providerIds = new Set([...members, ...snapshot.providerIds]);
|
|
3201
|
+
providerIds.delete(LOGICAL_PROVIDER_ID);
|
|
3136
3202
|
return {
|
|
3137
3203
|
resolution,
|
|
3138
|
-
members:
|
|
3139
|
-
const
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
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" };
|
|
3154
3223
|
}),
|
|
3155
3224
|
};
|
|
3156
3225
|
};
|
|
3157
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
|
+
|
|
3158
3242
|
const commands = new MultiAccountCommandController({
|
|
3159
3243
|
state,
|
|
3160
3244
|
usage,
|
|
@@ -3198,7 +3282,7 @@ export const createMultiAccountExtension =
|
|
|
3198
3282
|
sessionAccountGroups.clearOverride(sessionManager);
|
|
3199
3283
|
return resolveCommandAccountGroup();
|
|
3200
3284
|
},
|
|
3201
|
-
status:
|
|
3285
|
+
status: readCommandAccountGroupStatus,
|
|
3202
3286
|
},
|
|
3203
3287
|
logicalRoutingState: () => logicalRoutingState,
|
|
3204
3288
|
disabledProviders,
|
|
@@ -3283,9 +3367,12 @@ export const createMultiAccountExtension =
|
|
|
3283
3367
|
setModel,
|
|
3284
3368
|
rediscover,
|
|
3285
3369
|
addSlot,
|
|
3286
|
-
reloadGlobalConfig: async () =>
|
|
3370
|
+
reloadGlobalConfig: async () => loadGlobalConfig(),
|
|
3287
3371
|
onConfigReload: async (nextConfig) => {
|
|
3288
3372
|
config = nextConfig;
|
|
3373
|
+
policyConfigError = undefined;
|
|
3374
|
+
accountGroupResolutionInitialized = false;
|
|
3375
|
+
initializeSessionAccountGroupScope(context);
|
|
3289
3376
|
await rediscover();
|
|
3290
3377
|
},
|
|
3291
3378
|
routingConfig: {
|
|
@@ -3333,8 +3420,8 @@ export const createMultiAccountExtension =
|
|
|
3333
3420
|
// Group scope is stricter than the optional metered rung. For unrestricted
|
|
3334
3421
|
// sessions, consent, delegate exclusion, session disablement, model syntax,
|
|
3335
3422
|
// and the positive budget cap remain the authority for OpenRouter.
|
|
3336
|
-
// A named
|
|
3337
|
-
//
|
|
3423
|
+
// A named group remains closed to metered OpenRouter even when listed.
|
|
3424
|
+
// Membership alone cannot enable or widen the existing metered gate.
|
|
3338
3425
|
if (accountGroupScopeRestrictsRouting()) return false;
|
|
3339
3426
|
const policy = openRouterPolicy();
|
|
3340
3427
|
if (!policy.enabled) return false;
|
|
@@ -3476,12 +3563,31 @@ export const createMultiAccountExtension =
|
|
|
3476
3563
|
providerId: string,
|
|
3477
3564
|
inputContext: ExtensionContext,
|
|
3478
3565
|
): boolean => {
|
|
3566
|
+
if (policyConfigError !== undefined) return false;
|
|
3479
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;
|
|
3480
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;
|
|
3481
3589
|
try {
|
|
3482
|
-
return
|
|
3483
|
-
(account) => account.providerId === providerId,
|
|
3484
|
-
);
|
|
3590
|
+
return physicalOperatorAccounts(context ?? inputContext).some((account) => account.providerId === providerId);
|
|
3485
3591
|
} catch {
|
|
3486
3592
|
return false;
|
|
3487
3593
|
}
|
|
@@ -3526,12 +3632,29 @@ export const createMultiAccountExtension =
|
|
|
3526
3632
|
): Promise<{ readonly action: "continue" | "handled" }> => {
|
|
3527
3633
|
const activeProviderId =
|
|
3528
3634
|
inputContext.model?.provider ?? currentModel?.provider;
|
|
3635
|
+
if (policyConfigError !== undefined && activeProviderId === undefined) {
|
|
3636
|
+
inputContext.ui.notify(policyConfigError, "warning");
|
|
3637
|
+
return { action: "handled" };
|
|
3638
|
+
}
|
|
3529
3639
|
if (
|
|
3530
3640
|
activeProviderId === undefined ||
|
|
3531
3641
|
providerAllowedByActiveGroup(activeProviderId, inputContext)
|
|
3532
3642
|
) {
|
|
3533
3643
|
return { action: "continue" };
|
|
3534
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
|
+
}
|
|
3535
3658
|
const origin = accountGroupOrigin(
|
|
3536
3659
|
activeProviderId,
|
|
3537
3660
|
inputContext.model?.id ?? currentModel?.id,
|
|
@@ -3574,7 +3697,7 @@ export const createMultiAccountExtension =
|
|
|
3574
3697
|
replacement = undefined;
|
|
3575
3698
|
}
|
|
3576
3699
|
}
|
|
3577
|
-
if (replacement !== undefined) {
|
|
3700
|
+
if (replacement !== undefined && !isAccountGroupVirtualModel(replacement)) {
|
|
3578
3701
|
try {
|
|
3579
3702
|
if (await setModel(replacement)) {
|
|
3580
3703
|
if (origin !== undefined) {
|
|
@@ -3781,6 +3904,11 @@ export const createMultiAccountExtension =
|
|
|
3781
3904
|
*/
|
|
3782
3905
|
const settleTurn = async (): Promise<void> => {
|
|
3783
3906
|
openRouterInputApproved = false;
|
|
3907
|
+
if (policyConfigError !== undefined) {
|
|
3908
|
+
pendingFailure = undefined;
|
|
3909
|
+
pendingOpenRouterFailure = false;
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3784
3912
|
if (pendingOpenRouterFailure) {
|
|
3785
3913
|
pendingOpenRouterFailure = false;
|
|
3786
3914
|
const restored = await restoreOpenRouterOrigin();
|
|
@@ -3800,6 +3928,24 @@ export const createMultiAccountExtension =
|
|
|
3800
3928
|
const origin = turnRouteOrigin;
|
|
3801
3929
|
if (!classified || origin === undefined) return;
|
|
3802
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
|
+
}
|
|
3803
3949
|
const accounts = physicalOperatorAccounts();
|
|
3804
3950
|
const nowMs = Date.now();
|
|
3805
3951
|
const routingAccounts = physicalAccountsForRouting(nowMs);
|
|
@@ -4162,7 +4308,13 @@ export const createMultiAccountExtension =
|
|
|
4162
4308
|
const observedMessages = new WeakSet<object>();
|
|
4163
4309
|
// Operator advice for a structured refusal. A delegate-owned in-process
|
|
4164
4310
|
// session shares the foreground UI, so only the foreground advises.
|
|
4165
|
-
const refusalAdvisor = createRefusalAdvisor({
|
|
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
|
+
});
|
|
4166
4318
|
const recordUsage = (
|
|
4167
4319
|
observation: () => UsageObservation | undefined,
|
|
4168
4320
|
): void => {
|
|
@@ -4312,8 +4464,9 @@ export const createMultiAccountExtension =
|
|
|
4312
4464
|
const activeProviderId =
|
|
4313
4465
|
runContext.model?.provider ?? currentModel?.provider;
|
|
4314
4466
|
if (
|
|
4315
|
-
activeProviderId !== undefined
|
|
4316
|
-
|
|
4467
|
+
(activeProviderId === undefined && policyConfigError !== undefined) ||
|
|
4468
|
+
(activeProviderId !== undefined &&
|
|
4469
|
+
!providerAllowedByActiveGroup(activeProviderId, runContext))
|
|
4317
4470
|
) {
|
|
4318
4471
|
openRouterInputApproved = false;
|
|
4319
4472
|
diagnostics.record(
|
|
@@ -4322,11 +4475,11 @@ export const createMultiAccountExtension =
|
|
|
4322
4475
|
"A custom-message run selected a provider outside the active account group and was aborted before provider dispatch.",
|
|
4323
4476
|
{ providerId: activeProviderId },
|
|
4324
4477
|
);
|
|
4478
|
+
runContext.abort();
|
|
4325
4479
|
runContext.ui.notify(
|
|
4326
|
-
"Multi-account aborted a provider run outside the active account group.",
|
|
4480
|
+
policyConfigError ?? "Multi-account aborted a provider run outside the active account group.",
|
|
4327
4481
|
"warning",
|
|
4328
4482
|
);
|
|
4329
|
-
runContext.abort();
|
|
4330
4483
|
return;
|
|
4331
4484
|
}
|
|
4332
4485
|
const ownsActiveOpenRouter =
|
|
@@ -4355,6 +4508,10 @@ export const createMultiAccountExtension =
|
|
|
4355
4508
|
pi.on(
|
|
4356
4509
|
"session_start",
|
|
4357
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);
|
|
4358
4515
|
context = startupContext;
|
|
4359
4516
|
try {
|
|
4360
4517
|
logicalRouteIndicator?.shutdown();
|
|
@@ -4414,7 +4571,27 @@ export const createMultiAccountExtension =
|
|
|
4414
4571
|
now: Date.now,
|
|
4415
4572
|
});
|
|
4416
4573
|
currentModel = startupContext.model;
|
|
4417
|
-
|
|
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
|
+
}
|
|
4418
4595
|
// Resolve and durably cache the session policy before rediscovery can
|
|
4419
4596
|
// publish any request-capable provider surface.
|
|
4420
4597
|
accountGroupResolutionInitialized = false;
|
|
@@ -4579,10 +4756,32 @@ export const createMultiAccountExtension =
|
|
|
4579
4756
|
: decorateLogicalAttributionLifecycle(store, indicator);
|
|
4580
4757
|
},
|
|
4581
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
|
+
},
|
|
4582
4780
|
// A getter, not an array. `clear`, `disable`, expiry and
|
|
4583
4781
|
// provider-reported exhaustion must take effect on the next
|
|
4584
4782
|
// request, not at the next restart.
|
|
4585
4783
|
get accounts() {
|
|
4784
|
+
if (policyConfigError !== undefined) throw new Error(policyConfigError);
|
|
4586
4785
|
const nowMs = Date.now();
|
|
4587
4786
|
return logicalAccountsFromManaged(
|
|
4588
4787
|
physicalAccountsForRouting(nowMs),
|
|
@@ -4592,16 +4791,27 @@ export const createMultiAccountExtension =
|
|
|
4592
4791
|
dispatch: createLogicalDispatch(context.modelRegistry),
|
|
4593
4792
|
onPublicTerminal: logicalTerminalAssociations.bindPublicTerminal,
|
|
4594
4793
|
state,
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
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
|
+
};
|
|
4600
4805
|
},
|
|
4601
4806
|
modelVendor,
|
|
4602
4807
|
get tierModelMap() {
|
|
4603
4808
|
return config.tierModelMap;
|
|
4604
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
|
+
},
|
|
4605
4815
|
...(config.crossFamilyChainEnabled
|
|
4606
4816
|
? { crossFamilyChains: config.crossFamilyChains }
|
|
4607
4817
|
: {}),
|
|
@@ -4664,20 +4874,22 @@ export const createMultiAccountExtension =
|
|
|
4664
4874
|
// a fault in the logical gate must not take them down with it.
|
|
4665
4875
|
diagnostics.recordError("logical.registration", error);
|
|
4666
4876
|
}
|
|
4667
|
-
|
|
4668
|
-
.
|
|
4669
|
-
|
|
4670
|
-
(
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
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
|
+
}
|
|
4681
4893
|
// Pi restores the session model inside createAgentSession, strictly
|
|
4682
4894
|
// BEFORE extensions load, so a managed alias does not exist yet and the
|
|
4683
4895
|
// host falls back to a base provider. Now that the aliases are
|
|
@@ -4744,6 +4956,7 @@ export const createMultiAccountExtension =
|
|
|
4744
4956
|
guarded(
|
|
4745
4957
|
"lifecycle.before-agent-start",
|
|
4746
4958
|
async (_event, upcomingContext) => {
|
|
4959
|
+
if (policyConfigError !== undefined) return;
|
|
4747
4960
|
// The warmer and usage fetcher each project independently, so
|
|
4748
4961
|
// removing one call-site guard cannot expose the sibling sink.
|
|
4749
4962
|
void Promise.resolve()
|
|
@@ -4779,6 +4992,10 @@ export const createMultiAccountExtension =
|
|
|
4779
4992
|
const accounts = subscriptionOperatorAccounts(
|
|
4780
4993
|
context ?? upcomingContext,
|
|
4781
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
|
+
}
|
|
4782
4999
|
const nowMs = Date.now();
|
|
4783
5000
|
const managedAccounts: SubscriptionManagedAccount[] = accounts.map(
|
|
4784
5001
|
({
|
|
@@ -4995,6 +5212,137 @@ export const createMultiAccountExtension =
|
|
|
4995
5212
|
});
|
|
4996
5213
|
}),
|
|
4997
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
|
+
};
|
|
4998
5346
|
// Observation and classification only. Every switch, park, or resume waits
|
|
4999
5347
|
// for `agent_settled`; see `settleTurn` above.
|
|
5000
5348
|
pi.on("message_end", async (event, messageContext) => {
|