@elizaos/app-core 2.0.0-alpha.413 → 2.0.0-alpha.415
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/apps/app-lifeops/src/lifeops/service-mixin-whatsapp.js +1 -90
- package/package.json +5 -5
- package/packages/agent/src/actions/index.d.ts +0 -1
- package/packages/agent/src/actions/index.d.ts.map +1 -1
- package/packages/agent/src/actions/index.js +0 -1
- package/packages/agent/src/api/accounts-routes.d.ts +31 -0
- package/packages/agent/src/api/accounts-routes.d.ts.map +1 -0
- package/packages/agent/src/api/accounts-routes.js +745 -0
- package/packages/agent/src/api/index.d.ts +1 -0
- package/packages/agent/src/api/index.d.ts.map +1 -1
- package/packages/agent/src/api/index.js +1 -0
- package/packages/agent/src/api/model-provider-helpers.js +10 -10
- package/packages/agent/src/api/provider-switch-config.js +2 -2
- package/packages/agent/src/api/server.d.ts.map +1 -1
- package/packages/agent/src/api/server.js +14 -0
- package/packages/agent/src/api/subscription-routes.d.ts.map +1 -1
- package/packages/agent/src/api/subscription-routes.js +48 -1
- package/packages/agent/src/auth/credentials.d.ts.map +1 -1
- package/packages/agent/src/auth/credentials.js +14 -3
- package/packages/agent/src/auth/index.d.ts +2 -0
- package/packages/agent/src/auth/index.d.ts.map +1 -1
- package/packages/agent/src/auth/index.js +2 -0
- package/packages/agent/src/auth/oauth-flow.d.ts +106 -0
- package/packages/agent/src/auth/oauth-flow.d.ts.map +1 -0
- package/packages/agent/src/auth/oauth-flow.js +349 -0
- package/packages/agent/src/auth/vendor/pi-oauth/anthropic-login.d.ts +32 -0
- package/packages/agent/src/auth/vendor/pi-oauth/anthropic-login.d.ts.map +1 -1
- package/packages/agent/src/auth/vendor/pi-oauth/anthropic-login.js +63 -28
- package/packages/agent/src/config/schema.js +1 -1
- package/packages/agent/src/config/types.messages.d.ts +1 -1
- package/packages/agent/src/config/types.tools.d.ts +1 -1
- package/packages/agent/src/providers/page-scoped-context.js +1 -1
- package/packages/agent/src/runtime/eliza-plugin.d.ts.map +1 -1
- package/packages/agent/src/runtime/eliza-plugin.js +0 -3
- package/packages/agent/src/runtime/eliza.js +3 -3
- package/packages/agent/src/runtime/plugin-collector.js +3 -4
- package/packages/app-core/src/api/accounts-routes.d.ts +16 -0
- package/packages/app-core/src/api/accounts-routes.d.ts.map +1 -0
- package/packages/app-core/src/api/accounts-routes.js +15 -0
- package/packages/app-core/src/api/credential-resolver.d.ts +15 -0
- package/packages/app-core/src/api/credential-resolver.d.ts.map +1 -1
- package/packages/app-core/src/api/credential-resolver.js +43 -0
- package/packages/app-core/src/api/plugins-compat-routes.d.ts +1 -0
- package/packages/app-core/src/api/plugins-compat-routes.d.ts.map +1 -1
- package/packages/app-core/src/api/plugins-compat-routes.js +5 -3
- package/packages/app-core/src/components/conversations/conversation-utils.js +4 -4
- package/packages/app-core/src/components/pages/page-scoped-conversations.js +1 -1
- package/packages/app-core/src/components/settings/ProviderSwitcher.js +1 -1
- package/packages/app-core/src/services/account-pool.d.ts +91 -0
- package/packages/app-core/src/services/account-pool.d.ts.map +1 -0
- package/packages/app-core/src/services/account-pool.js +466 -0
- package/packages/app-core/src/services/account-usage.d.ts +73 -0
- package/packages/app-core/src/services/account-usage.d.ts.map +1 -0
- package/packages/app-core/src/services/account-usage.js +179 -0
- package/packages/app-core/src/services/auth-store.js +1 -1
- package/packages/app-core/src/state/useOnboardingState.js +1 -1
- package/packages/shared/src/contracts/lifeops.d.ts +5 -4
- package/packages/shared/src/contracts/lifeops.d.ts.map +1 -1
- package/packages/typescript/src/utils/context-catalog.d.ts.map +1 -1
- package/packages/typescript/src/utils/context-catalog.js +2 -3
- package/packages/agent/src/actions/app-control.d.ts +0 -23
- package/packages/agent/src/actions/app-control.d.ts.map +0 -1
- package/packages/agent/src/actions/app-control.js +0 -775
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account selection brain.
|
|
3
|
+
*
|
|
4
|
+
* Owns the runtime decision "which `LinkedAccountConfig` should serve this
|
|
5
|
+
* request?" given a strategy (priority / round-robin / least-used /
|
|
6
|
+
* quota-aware), session affinity, and per-account health state.
|
|
7
|
+
*
|
|
8
|
+
* The pool never reads OAuth credentials directly — callers resolve them
|
|
9
|
+
* via `getAccessToken(providerId, accountId)` from `@elizaos/agent` once
|
|
10
|
+
* the pool returns an account. Health, priority, and usage live in this
|
|
11
|
+
* layer; the OAuth blob lives under `~/.eliza/auth/` (see WS1's
|
|
12
|
+
* `account-storage.ts`).
|
|
13
|
+
*
|
|
14
|
+
* Persistence: the pool layers rich metadata (priority, enabled, health,
|
|
15
|
+
* usage) on top of WS1's credential records. The metadata is written to
|
|
16
|
+
* `<ELIZA_HOME>/auth/_pool-metadata.json` atomically so it survives
|
|
17
|
+
* process restarts and is independent of WS3's eventual `milady.json`
|
|
18
|
+
* field — when WS3 lands its CRUD API on top of `LinkedAccountsConfig`
|
|
19
|
+
* we can swap `createDefaultAccountPool()`'s deps without touching the
|
|
20
|
+
* pool itself.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { getAccessToken as getSubscriptionAccessToken, listProviderAccounts, } from "@elizaos/agent";
|
|
26
|
+
import { pollAnthropicUsage, pollCodexUsage, recordCall as recordUsageEntry, } from "./account-usage.js";
|
|
27
|
+
const DEFAULT_RATE_LIMIT_BACKOFF_MS = 60_000;
|
|
28
|
+
const QUOTA_AWARE_SKIP_PCT = 85;
|
|
29
|
+
const SESSION_AFFINITY_MAX_ATTEMPTS = 3;
|
|
30
|
+
export class AccountPool {
|
|
31
|
+
deps;
|
|
32
|
+
affinity = new Map();
|
|
33
|
+
roundRobinCursor = new Map();
|
|
34
|
+
constructor(deps) {
|
|
35
|
+
this.deps = deps;
|
|
36
|
+
}
|
|
37
|
+
// Selection.
|
|
38
|
+
async select(input) {
|
|
39
|
+
const all = this.deps.readAccounts();
|
|
40
|
+
const eligible = this.filterEligible(all, input);
|
|
41
|
+
if (eligible.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
if (input.sessionKey) {
|
|
44
|
+
const cached = this.affinity.get(input.sessionKey);
|
|
45
|
+
if (cached &&
|
|
46
|
+
cached.attempts < SESSION_AFFINITY_MAX_ATTEMPTS &&
|
|
47
|
+
eligible.some((a) => a.id === cached.accountId)) {
|
|
48
|
+
cached.attempts += 1;
|
|
49
|
+
const account = eligible.find((a) => a.id === cached.accountId);
|
|
50
|
+
if (account)
|
|
51
|
+
return account;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const strategy = input.strategy ?? "priority";
|
|
55
|
+
const picked = this.applyStrategy(strategy, eligible, input.providerId);
|
|
56
|
+
if (!picked)
|
|
57
|
+
return null;
|
|
58
|
+
if (input.sessionKey) {
|
|
59
|
+
this.affinity.set(input.sessionKey, {
|
|
60
|
+
accountId: picked.id,
|
|
61
|
+
attempts: 1,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return picked;
|
|
65
|
+
}
|
|
66
|
+
filterEligible(all, input) {
|
|
67
|
+
const exclude = new Set(input.exclude ?? []);
|
|
68
|
+
const explicit = input.accountIds && input.accountIds.length > 0
|
|
69
|
+
? new Set(input.accountIds)
|
|
70
|
+
: null;
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
return Object.values(all).filter((account) => {
|
|
73
|
+
if (account.providerId !== input.providerId)
|
|
74
|
+
return false;
|
|
75
|
+
if (!account.enabled)
|
|
76
|
+
return false;
|
|
77
|
+
if (exclude.has(account.id))
|
|
78
|
+
return false;
|
|
79
|
+
if (explicit && !explicit.has(account.id))
|
|
80
|
+
return false;
|
|
81
|
+
if (account.health === "ok")
|
|
82
|
+
return true;
|
|
83
|
+
// Allow rate-limited accounts back in once their reset has passed.
|
|
84
|
+
if (account.health === "rate-limited" &&
|
|
85
|
+
typeof account.healthDetail?.until === "number" &&
|
|
86
|
+
account.healthDetail.until < now) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
applyStrategy(strategy, eligible, providerId) {
|
|
93
|
+
if (eligible.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
if (eligible.length === 1)
|
|
96
|
+
return eligible[0] ?? null;
|
|
97
|
+
switch (strategy) {
|
|
98
|
+
case "round-robin": {
|
|
99
|
+
const sorted = [...eligible].sort(byPriorityThenAge);
|
|
100
|
+
const cursor = (this.roundRobinCursor.get(providerId) ?? -1) + 1;
|
|
101
|
+
const index = cursor % sorted.length;
|
|
102
|
+
this.roundRobinCursor.set(providerId, index);
|
|
103
|
+
return sorted[index] ?? null;
|
|
104
|
+
}
|
|
105
|
+
case "least-used": {
|
|
106
|
+
return [...eligible].sort(byLeastUsedThenPriority)[0] ?? null;
|
|
107
|
+
}
|
|
108
|
+
case "quota-aware": {
|
|
109
|
+
const underQuota = eligible.filter((a) => (a.usage?.sessionPct ?? 0) < QUOTA_AWARE_SKIP_PCT);
|
|
110
|
+
const pool = underQuota.length > 0 ? underQuota : eligible;
|
|
111
|
+
return [...pool].sort(byPriorityThenAge)[0] ?? null;
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
return [...eligible].sort(byPriorityThenAge)[0] ?? null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Mutations.
|
|
118
|
+
async recordCall(accountId, result) {
|
|
119
|
+
const account = this.deps.readAccounts()[accountId];
|
|
120
|
+
if (!account)
|
|
121
|
+
return;
|
|
122
|
+
recordUsageEntry(account.providerId, account.id, result);
|
|
123
|
+
const next = {
|
|
124
|
+
...account,
|
|
125
|
+
lastUsedAt: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
await this.deps.writeAccount(next);
|
|
128
|
+
}
|
|
129
|
+
async refreshUsage(accountId, accessToken, opts) {
|
|
130
|
+
const account = this.deps.readAccounts()[accountId];
|
|
131
|
+
if (!account)
|
|
132
|
+
return;
|
|
133
|
+
let usage;
|
|
134
|
+
if (account.providerId === "anthropic-subscription") {
|
|
135
|
+
usage = await pollAnthropicUsage(accessToken);
|
|
136
|
+
}
|
|
137
|
+
else if (account.providerId === "openai-codex") {
|
|
138
|
+
const codexAccountId = opts?.codexAccountId ?? account.organizationId;
|
|
139
|
+
if (!codexAccountId) {
|
|
140
|
+
throw new Error(`[AccountPool] Codex usage probe needs the OpenAI account_id (account ${accountId} has no organizationId).`);
|
|
141
|
+
}
|
|
142
|
+
usage = await pollCodexUsage(accessToken, codexAccountId);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// No probe defined for direct API providers.
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await this.deps.writeAccount({
|
|
149
|
+
...account,
|
|
150
|
+
health: "ok",
|
|
151
|
+
usage,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async markRateLimited(accountId, untilMs, detail) {
|
|
155
|
+
const account = this.deps.readAccounts()[accountId];
|
|
156
|
+
if (!account)
|
|
157
|
+
return;
|
|
158
|
+
const healthDetail = {
|
|
159
|
+
until: Number.isFinite(untilMs) && untilMs > Date.now()
|
|
160
|
+
? untilMs
|
|
161
|
+
: Date.now() + DEFAULT_RATE_LIMIT_BACKOFF_MS,
|
|
162
|
+
lastChecked: Date.now(),
|
|
163
|
+
...(detail ? { lastError: detail } : {}),
|
|
164
|
+
};
|
|
165
|
+
await this.deps.writeAccount({
|
|
166
|
+
...account,
|
|
167
|
+
health: "rate-limited",
|
|
168
|
+
healthDetail,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async markNeedsReauth(accountId, detail) {
|
|
172
|
+
const account = this.deps.readAccounts()[accountId];
|
|
173
|
+
if (!account)
|
|
174
|
+
return;
|
|
175
|
+
await this.deps.writeAccount({
|
|
176
|
+
...account,
|
|
177
|
+
health: "needs-reauth",
|
|
178
|
+
healthDetail: {
|
|
179
|
+
lastChecked: Date.now(),
|
|
180
|
+
...(detail ? { lastError: detail } : {}),
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async markInvalid(accountId, detail) {
|
|
185
|
+
const account = this.deps.readAccounts()[accountId];
|
|
186
|
+
if (!account)
|
|
187
|
+
return;
|
|
188
|
+
await this.deps.writeAccount({
|
|
189
|
+
...account,
|
|
190
|
+
health: "invalid",
|
|
191
|
+
healthDetail: {
|
|
192
|
+
lastChecked: Date.now(),
|
|
193
|
+
...(detail ? { lastError: detail } : {}),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async markHealthy(accountId) {
|
|
198
|
+
const account = this.deps.readAccounts()[accountId];
|
|
199
|
+
if (!account)
|
|
200
|
+
return;
|
|
201
|
+
if (account.health === "ok")
|
|
202
|
+
return;
|
|
203
|
+
await this.deps.writeAccount({
|
|
204
|
+
...account,
|
|
205
|
+
health: "ok",
|
|
206
|
+
...(account.healthDetail ? { healthDetail: undefined } : {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Re-probe accounts whose `health` is non-OK and whose `healthDetail.until`
|
|
211
|
+
* has passed (or is absent). Used by background sweepers to recover
|
|
212
|
+
* temporarily flagged accounts. We don't load access tokens here — the
|
|
213
|
+
* caller probes via `refreshUsage` separately.
|
|
214
|
+
*/
|
|
215
|
+
async reprobeFlagged() {
|
|
216
|
+
const all = this.deps.readAccounts();
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const ready = [];
|
|
219
|
+
for (const account of Object.values(all)) {
|
|
220
|
+
if (account.health === "ok")
|
|
221
|
+
continue;
|
|
222
|
+
if (account.health === "rate-limited") {
|
|
223
|
+
const until = account.healthDetail?.until;
|
|
224
|
+
if (typeof until === "number" && until > now)
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
ready.push(account.id);
|
|
228
|
+
}
|
|
229
|
+
return ready;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function byPriorityThenAge(a, b) {
|
|
233
|
+
if (a.priority !== b.priority)
|
|
234
|
+
return a.priority - b.priority;
|
|
235
|
+
const aLast = a.lastUsedAt ?? 0;
|
|
236
|
+
const bLast = b.lastUsedAt ?? 0;
|
|
237
|
+
return aLast - bLast; // older first
|
|
238
|
+
}
|
|
239
|
+
function byLeastUsedThenPriority(a, b) {
|
|
240
|
+
const aPct = a.usage?.sessionPct ?? 0;
|
|
241
|
+
const bPct = b.usage?.sessionPct ?? 0;
|
|
242
|
+
if (aPct !== bPct)
|
|
243
|
+
return aPct - bPct;
|
|
244
|
+
return byPriorityThenAge(a, b);
|
|
245
|
+
}
|
|
246
|
+
function authRoot() {
|
|
247
|
+
return path.join(process.env.ELIZA_HOME || path.join(os.homedir(), ".eliza"), "auth");
|
|
248
|
+
}
|
|
249
|
+
function metadataFile() {
|
|
250
|
+
return path.join(authRoot(), "_pool-metadata.json");
|
|
251
|
+
}
|
|
252
|
+
function isPoolProviderId(value) {
|
|
253
|
+
return (value === "anthropic-subscription" ||
|
|
254
|
+
value === "openai-codex" ||
|
|
255
|
+
value === "anthropic-api" ||
|
|
256
|
+
value === "openai-api");
|
|
257
|
+
}
|
|
258
|
+
function readMetaStore() {
|
|
259
|
+
const file = metadataFile();
|
|
260
|
+
if (!existsSync(file)) {
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const raw = readFileSync(file, "utf-8");
|
|
265
|
+
const parsed = JSON.parse(raw);
|
|
266
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
267
|
+
return parsed;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Corrupt file — fall through to empty store. Next write rewrites it.
|
|
272
|
+
}
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
function writeMetaStore(store) {
|
|
276
|
+
const file = metadataFile();
|
|
277
|
+
const dir = path.dirname(file);
|
|
278
|
+
if (!existsSync(dir)) {
|
|
279
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
280
|
+
}
|
|
281
|
+
const tmp = `${file}.tmp`;
|
|
282
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2), {
|
|
283
|
+
encoding: "utf-8",
|
|
284
|
+
mode: 0o600,
|
|
285
|
+
});
|
|
286
|
+
renameSync(tmp, file);
|
|
287
|
+
}
|
|
288
|
+
function recordToLinked(record, meta, providerId, defaultPriority) {
|
|
289
|
+
return {
|
|
290
|
+
id: record.id,
|
|
291
|
+
providerId,
|
|
292
|
+
label: meta?.label ?? record.label,
|
|
293
|
+
source: record.source,
|
|
294
|
+
enabled: meta?.enabled ?? true,
|
|
295
|
+
priority: meta?.priority ?? defaultPriority,
|
|
296
|
+
createdAt: record.createdAt,
|
|
297
|
+
health: meta?.health ?? "ok",
|
|
298
|
+
...(record.lastUsedAt !== undefined
|
|
299
|
+
? { lastUsedAt: record.lastUsedAt }
|
|
300
|
+
: {}),
|
|
301
|
+
...(meta?.healthDetail ? { healthDetail: meta.healthDetail } : {}),
|
|
302
|
+
...(meta?.usage ? { usage: meta.usage } : {}),
|
|
303
|
+
...(record.organizationId
|
|
304
|
+
? { organizationId: record.organizationId }
|
|
305
|
+
: {}),
|
|
306
|
+
...(record.userId ? { userId: record.userId } : {}),
|
|
307
|
+
...(record.email ? { email: record.email } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function loadAllAccounts() {
|
|
311
|
+
const subscriptionProviders = [
|
|
312
|
+
"anthropic-subscription",
|
|
313
|
+
"openai-codex",
|
|
314
|
+
];
|
|
315
|
+
const meta = readMetaStore();
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const provider of subscriptionProviders) {
|
|
318
|
+
const records = listProviderAccounts(provider);
|
|
319
|
+
let priorityCounter = 0;
|
|
320
|
+
const sorted = [...records].sort((a, b) => a.createdAt - b.createdAt);
|
|
321
|
+
for (const record of sorted) {
|
|
322
|
+
const providerMeta = meta[provider]?.[record.id];
|
|
323
|
+
out[record.id] = recordToLinked(record, providerMeta, provider, priorityCounter);
|
|
324
|
+
priorityCounter += 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
async function persistAccount(account) {
|
|
330
|
+
if (!isPoolProviderId(account.providerId))
|
|
331
|
+
return;
|
|
332
|
+
const store = readMetaStore();
|
|
333
|
+
if (!store[account.providerId]) {
|
|
334
|
+
store[account.providerId] = {};
|
|
335
|
+
}
|
|
336
|
+
store[account.providerId][account.id] = {
|
|
337
|
+
label: account.label,
|
|
338
|
+
enabled: account.enabled,
|
|
339
|
+
priority: account.priority,
|
|
340
|
+
health: account.health,
|
|
341
|
+
...(account.healthDetail ? { healthDetail: account.healthDetail } : {}),
|
|
342
|
+
...(account.usage ? { usage: account.usage } : {}),
|
|
343
|
+
};
|
|
344
|
+
writeMetaStore(store);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Symbol-keyed shim contract consumed by plugin-anthropic's
|
|
348
|
+
* `credential-store.ts`. Kept narrow so the plugin doesn't have to import
|
|
349
|
+
* the full pool surface (or the rest of `@elizaos/app-core`).
|
|
350
|
+
*/
|
|
351
|
+
const ANTHROPIC_POOL_SHIM_SYMBOL = Symbol.for("milady.account-pool.anthropic.v1");
|
|
352
|
+
/**
|
|
353
|
+
* Shim used by plugin-agent-orchestrator. The orchestrator can't depend on
|
|
354
|
+
* `@elizaos/app-core`, so it discovers the pool via this symbol on
|
|
355
|
+
* `globalThis`. Returns the picked account + access token in one shot
|
|
356
|
+
* because the orchestrator only needs to inject the env vars and forget.
|
|
357
|
+
*/
|
|
358
|
+
const ORCHESTRATOR_POOL_SHIM_SYMBOL = Symbol.for("milady.account-pool.orchestrator.v1");
|
|
359
|
+
/**
|
|
360
|
+
* Shim used by `applySubscriptionCredentials` in `@elizaos/agent` to pick
|
|
361
|
+
* the active Codex account when applying `OPENAI_API_KEY`. Lives behind
|
|
362
|
+
* a symbol so the agent package doesn't need to depend on app-core.
|
|
363
|
+
*/
|
|
364
|
+
const SUBSCRIPTION_SELECTOR_SHIM_SYMBOL = Symbol.for("milady.account-pool.subscription-selector.v1");
|
|
365
|
+
let cachedDefaultPool = null;
|
|
366
|
+
/**
|
|
367
|
+
* Module-level singleton for the default pool wired against WS1's
|
|
368
|
+
* `account-storage` and the pool-owned metadata file. Plugins / runtime
|
|
369
|
+
* resolvers should import `getDefaultAccountPool()` rather than building
|
|
370
|
+
* a new pool. WS3 may later swap the default deps to read/write the
|
|
371
|
+
* `LinkedAccountsConfig` field directly out of `milady.json`; consumers
|
|
372
|
+
* keep the same accessor.
|
|
373
|
+
*/
|
|
374
|
+
export function getDefaultAccountPool() {
|
|
375
|
+
if (!cachedDefaultPool) {
|
|
376
|
+
cachedDefaultPool = new AccountPool({
|
|
377
|
+
readAccounts: () => loadAllAccounts(),
|
|
378
|
+
writeAccount: persistAccount,
|
|
379
|
+
});
|
|
380
|
+
installAnthropicShim(cachedDefaultPool);
|
|
381
|
+
installOrchestratorShim(cachedDefaultPool);
|
|
382
|
+
installSubscriptionSelectorShim(cachedDefaultPool);
|
|
383
|
+
}
|
|
384
|
+
return cachedDefaultPool;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Install the `globalThis`-keyed shim that plugin-anthropic's
|
|
388
|
+
* credential-store reads. Idempotent — repeated installs replace the
|
|
389
|
+
* previous shim.
|
|
390
|
+
*/
|
|
391
|
+
function installAnthropicShim(pool) {
|
|
392
|
+
if (typeof globalThis === "undefined")
|
|
393
|
+
return;
|
|
394
|
+
const shim = {
|
|
395
|
+
selectAnthropicSubscription: async (opts) => {
|
|
396
|
+
const account = await pool.select({
|
|
397
|
+
providerId: "anthropic-subscription",
|
|
398
|
+
sessionKey: opts?.sessionKey,
|
|
399
|
+
exclude: opts?.exclude,
|
|
400
|
+
});
|
|
401
|
+
if (!account)
|
|
402
|
+
return null;
|
|
403
|
+
// expiresAt is sourced from the underlying credential blob via
|
|
404
|
+
// `loadCredentials`; we cache it on the cached account record's
|
|
405
|
+
// lastUsedAt is independent. The plugin only uses expiresAt as a
|
|
406
|
+
// hint for cache TTL, so an Infinity fallback is acceptable.
|
|
407
|
+
return { id: account.id, expiresAt: Number.POSITIVE_INFINITY };
|
|
408
|
+
},
|
|
409
|
+
getAccessToken: (providerId, accountId) => getSubscriptionAccessToken(providerId, accountId),
|
|
410
|
+
markInvalid: (accountId, detail) => pool.markInvalid(accountId, detail),
|
|
411
|
+
markRateLimited: (accountId, untilMs, detail) => pool.markRateLimited(accountId, untilMs, detail),
|
|
412
|
+
};
|
|
413
|
+
globalThis[ANTHROPIC_POOL_SHIM_SYMBOL] = shim;
|
|
414
|
+
}
|
|
415
|
+
function installOrchestratorShim(pool) {
|
|
416
|
+
if (typeof globalThis === "undefined")
|
|
417
|
+
return;
|
|
418
|
+
const shim = {
|
|
419
|
+
pickAnthropicTokenForSpawn: async ({ sessionKey }) => {
|
|
420
|
+
const account = await pool.select({
|
|
421
|
+
providerId: "anthropic-subscription",
|
|
422
|
+
sessionKey,
|
|
423
|
+
});
|
|
424
|
+
if (!account)
|
|
425
|
+
return null;
|
|
426
|
+
const token = await getSubscriptionAccessToken("anthropic-subscription", account.id);
|
|
427
|
+
if (!token)
|
|
428
|
+
return null;
|
|
429
|
+
return { accessToken: token, accountId: account.id };
|
|
430
|
+
},
|
|
431
|
+
markRateLimited: (accountId, untilMs, detail) => {
|
|
432
|
+
void pool.markRateLimited(accountId, untilMs, detail);
|
|
433
|
+
},
|
|
434
|
+
markInvalid: (accountId, detail) => {
|
|
435
|
+
void pool.markInvalid(accountId, detail);
|
|
436
|
+
},
|
|
437
|
+
markNeedsReauth: (accountId, detail) => {
|
|
438
|
+
void pool.markNeedsReauth(accountId, detail);
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
globalThis[ORCHESTRATOR_POOL_SHIM_SYMBOL] = shim;
|
|
442
|
+
}
|
|
443
|
+
function installSubscriptionSelectorShim(pool) {
|
|
444
|
+
if (typeof globalThis === "undefined")
|
|
445
|
+
return;
|
|
446
|
+
const shim = {
|
|
447
|
+
pickAccountId: async (providerId) => {
|
|
448
|
+
const account = await pool.select({ providerId });
|
|
449
|
+
return account?.id ?? null;
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
globalThis[SUBSCRIPTION_SELECTOR_SHIM_SYMBOL] = shim;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* @deprecated kept for compatibility with the WS2 spec naming. Use
|
|
456
|
+
* {@link getDefaultAccountPool}.
|
|
457
|
+
*/
|
|
458
|
+
export function createDefaultAccountPool() {
|
|
459
|
+
return getDefaultAccountPool();
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Resets the cached singleton. Test-only.
|
|
463
|
+
*/
|
|
464
|
+
export function __resetDefaultAccountPoolForTests() {
|
|
465
|
+
cachedDefaultPool = null;
|
|
466
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account usage probes + local JSONL counters.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
* 1. Probe provider usage APIs (`pollAnthropicUsage`, `pollCodexUsage`)
|
|
6
|
+
* to populate the `LinkedAccountUsage` snapshot on each account.
|
|
7
|
+
* 2. Maintain append-only JSONL counters per `(providerId, accountId, day)`
|
|
8
|
+
* so we can answer "calls made today / tokens used / errors" without
|
|
9
|
+
* re-reading every trajectory.
|
|
10
|
+
*
|
|
11
|
+
* The probes throw on HTTP error so the caller can decide whether to mark
|
|
12
|
+
* the account as `rate-limited` / `needs-reauth` / `invalid`. The counters
|
|
13
|
+
* are best-effort and synchronous — at our scale appendFileSync is fine.
|
|
14
|
+
*/
|
|
15
|
+
import type { LinkedAccountUsage } from "@elizaos/shared";
|
|
16
|
+
/**
|
|
17
|
+
* Snapshot returned by the provider usage probes. Mirrors
|
|
18
|
+
* {@link LinkedAccountUsage} but without `refreshedAt` being optional —
|
|
19
|
+
* the probe is the thing that stamps it.
|
|
20
|
+
*/
|
|
21
|
+
export interface UsageSnapshot extends LinkedAccountUsage {
|
|
22
|
+
refreshedAt: number;
|
|
23
|
+
}
|
|
24
|
+
export interface UsageEntry {
|
|
25
|
+
ts: number;
|
|
26
|
+
tokens?: number;
|
|
27
|
+
latencyMs?: number;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
model?: string;
|
|
30
|
+
errorCode?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Probe Anthropic's OAuth usage endpoint.
|
|
34
|
+
*
|
|
35
|
+
* Endpoint: `GET https://api.anthropic.com/api/oauth/usage`
|
|
36
|
+
* Headers : `Authorization: Bearer <accessToken>`,
|
|
37
|
+
* `anthropic-beta: oauth-2025-04-20`,
|
|
38
|
+
* `Content-Type: application/json`
|
|
39
|
+
*
|
|
40
|
+
* Handles both legacy flat (`five_hour_utilization`) and new nested
|
|
41
|
+
* (`five_hour: { utilization }`) response shapes. Throws on any HTTP
|
|
42
|
+
* error with the status code included in the message.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pollAnthropicUsage(accessToken: string): Promise<UsageSnapshot>;
|
|
45
|
+
/**
|
|
46
|
+
* Probe Codex / ChatGPT's usage endpoint.
|
|
47
|
+
*
|
|
48
|
+
* Endpoint: `GET https://chatgpt.com/backend-api/wham/usage`
|
|
49
|
+
* Headers : `Authorization: Bearer <accessToken>`,
|
|
50
|
+
* `ChatGPT-Account-Id: <openAIAccountId>`,
|
|
51
|
+
* `User-Agent: codex-cli`
|
|
52
|
+
*
|
|
53
|
+
* `used_percent` is already on the 0..100 scale. `reset_at` is epoch
|
|
54
|
+
* seconds. Codex has no weekly equivalent, so `weeklyPct` stays undefined.
|
|
55
|
+
*/
|
|
56
|
+
export declare function pollCodexUsage(accessToken: string, accountId: string): Promise<UsageSnapshot>;
|
|
57
|
+
/**
|
|
58
|
+
* Append a usage entry for the given `(providerId, accountId)` pair.
|
|
59
|
+
* One line per call, written synchronously with mode 0o600. The day
|
|
60
|
+
* directory is created on demand.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordCall(providerId: string, accountId: string, entry: Omit<UsageEntry, "ts">): void;
|
|
63
|
+
export interface DailyCounters {
|
|
64
|
+
calls: number;
|
|
65
|
+
tokens: number;
|
|
66
|
+
errors: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read today's JSONL and aggregate `(calls, tokens, errors)`. Lines that
|
|
70
|
+
* fail to parse are skipped silently (best-effort).
|
|
71
|
+
*/
|
|
72
|
+
export declare function readTodayCounters(providerId: string, accountId: string): DailyCounters;
|
|
73
|
+
//# sourceMappingURL=account-usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-usage.d.ts","sourceRoot":"","sources":["../../../../../src/services/account-usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,kBAAkB;IACvD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsCD;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,aAAa,CAAC,CAiCxB;AAaD;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC,CA6BxB;AA8BD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAC5B,IAAI,CAYN;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,aAAa,CA0Bf"}
|