@wayner6/pi-usage 0.1.3 → 0.1.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Metric, UsageAdapter, UsageSnapshot } from "../../../core/types.ts";
|
|
2
2
|
import { bridgeUrl, safeError, sameOriginFetch } from "../../../core/security.ts";
|
|
3
|
-
import { isAccountRelevantToModels } from "../matching.ts";
|
|
3
|
+
import { isAccountRelevantToModels, isGroupRelevantToModels, deduplicateSharedQuotaGroups } from "../matching.ts";
|
|
4
4
|
|
|
5
5
|
const NATIVE_PROVIDER_IDS = new Set(["deepseek", "openai-codex", "xai", "anthropic", "glm", "zai", "zai-coding-cn"]);
|
|
6
6
|
|
|
@@ -64,22 +64,35 @@ export const cliProxyBridgeAdapter: UsageAdapter = {
|
|
|
64
64
|
if (!response.ok) throw new Error(`pi-bridge returned HTTP ${response.status}`);
|
|
65
65
|
const data = await response.json() as BridgeUsage;
|
|
66
66
|
if (data.schemaVersion !== 1) return { adapterId: this.id, sourceProviderId: target.providerId, displayName: target.providerId, state: "incompatible", fetchedAt, accounts: [], error: `Unsupported pi-bridge schemaVersion ${String(data.schemaVersion)}` };
|
|
67
|
-
let accounts = (data.accounts ?? [])
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
label: account.label || account.account || account.provider || `Account ${index + 1}`,
|
|
71
|
-
...(account.status ? { status: account.status } : {}),
|
|
72
|
-
...(account.disabled !== undefined ? { disabled: account.disabled } : {}),
|
|
73
|
-
...(account.unavailable !== undefined ? { unavailable: account.unavailable } : {}),
|
|
74
|
-
metrics: metrics(account.groups ?? []),
|
|
75
|
-
rawGroups: account.groups,
|
|
76
|
-
...(account.error ? { error: account.error } : {}),
|
|
77
|
-
}));
|
|
67
|
+
let accounts = (data.accounts ?? [])
|
|
68
|
+
.map((account, index) => {
|
|
69
|
+
let rawGroups = account.groups ?? [];
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
// 1. Filter groups within this account if user configured specific models
|
|
72
|
+
if (target.configuredModelIds && target.configuredModelIds.length > 0) {
|
|
73
|
+
rawGroups = rawGroups.filter((g) => isGroupRelevantToModels(g, target.configuredModelIds));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Deduplicate shared quota pools (groups with identical remaining fraction and reset time)
|
|
77
|
+
const deduplicatedGroups = deduplicateSharedQuotaGroups(rawGroups);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: account.authIndex ?? `${account.provider ?? "provider"}-${index}`,
|
|
81
|
+
provider: account.provider ?? "unknown",
|
|
82
|
+
label: account.label || account.account || account.provider || `Account ${index + 1}`,
|
|
83
|
+
...(account.status ? { status: account.status } : {}),
|
|
84
|
+
...(account.disabled !== undefined ? { disabled: account.disabled } : {}),
|
|
85
|
+
...(account.unavailable !== undefined ? { unavailable: account.unavailable } : {}),
|
|
86
|
+
metrics: metrics(deduplicatedGroups),
|
|
87
|
+
rawGroups: deduplicatedGroups,
|
|
88
|
+
...(account.error ? { error: account.error } : {}),
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
// 3. Filter out accounts that have no relevant groups or aren't relevant to user models
|
|
92
|
+
.filter((account) => {
|
|
93
|
+
if (!target.configuredModelIds || target.configuredModelIds.length === 0) return true;
|
|
94
|
+
return isAccountRelevantToModels(account, target.configuredModelIds) && account.metrics.length > 0;
|
|
95
|
+
});
|
|
83
96
|
|
|
84
97
|
return {
|
|
85
98
|
adapterId: this.id,
|
|
@@ -75,17 +75,45 @@ export class ProviderUsageController {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async refreshAll(ctx: ExtensionContext, force = false): Promise<UsageSnapshot[]> {
|
|
78
|
-
const providerIds = new Set(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
const providerIds = new Set<string>();
|
|
79
|
+
|
|
80
|
+
// 1. Providers that have available models registered
|
|
81
|
+
for (const model of ctx.modelRegistry.getAvailable()) {
|
|
82
|
+
if (model.provider) providerIds.add(model.provider);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Providers explicitly registered or configured in auth
|
|
86
|
+
for (const id of ctx.modelRegistry.getRegisteredProviderIds()) {
|
|
87
|
+
// Only include if provider is actively configured with credentials
|
|
88
|
+
if (ctx.modelRegistry.getProviderAuthStatus(id).configured) {
|
|
89
|
+
providerIds.add(id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Known standard providers with configured auth
|
|
94
|
+
const knownProviders = [
|
|
95
|
+
"deepseek",
|
|
96
|
+
"openai-codex",
|
|
97
|
+
"xai",
|
|
98
|
+
"anthropic",
|
|
99
|
+
"zai-coding-cn",
|
|
100
|
+
"zai",
|
|
101
|
+
"glm",
|
|
102
|
+
];
|
|
103
|
+
for (const id of knownProviders) {
|
|
104
|
+
if (ctx.modelRegistry.getProviderAuthStatus(id).configured) {
|
|
105
|
+
providerIds.add(id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Config overrides & active model provider
|
|
110
|
+
for (const id of Object.keys(this.config.providerOverrides)) {
|
|
111
|
+
providerIds.add(id);
|
|
112
|
+
}
|
|
113
|
+
if (ctx.model?.provider) {
|
|
114
|
+
providerIds.add(ctx.model.provider);
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
const targets = await Promise.all([...providerIds].map((id) => this.target(ctx, id)));
|
|
90
118
|
const snapshots = await Promise.all(targets.map((target) => this.fetchTarget(target, force)));
|
|
91
119
|
return snapshots.filter((snapshot) => snapshot.state !== "unsupported");
|
|
@@ -290,3 +290,170 @@ export function isAccountRelevantToModels(
|
|
|
290
290
|
return false;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Determines whether a specific group in a proxy account is relevant to
|
|
295
|
+
* the user's configured models.
|
|
296
|
+
*/
|
|
297
|
+
export function isGroupRelevantToModels(
|
|
298
|
+
group: RawBridgeGroup,
|
|
299
|
+
configuredModelIds?: string[],
|
|
300
|
+
): boolean {
|
|
301
|
+
if (!configuredModelIds || configuredModelIds.length === 0) return true;
|
|
302
|
+
|
|
303
|
+
const targetTokens = new Set(configuredModelIds.flatMap(tokenizeModelId));
|
|
304
|
+
const modelKeywords = [
|
|
305
|
+
"claude", "gemini", "codex", "gpt", "openai", "deepseek", "kimi", "moonshot", "grok", "xai",
|
|
306
|
+
"thinking", "flash", "pro", "opus", "sonnet", "haiku", "turbo", "mini", "reasoning",
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
// Distinct tier tokens that shouldn't cross-match (e.g. 'pro' vs 'flash')
|
|
310
|
+
const tierTokens = ["pro", "flash", "thinking", "opus", "sonnet", "haiku"];
|
|
311
|
+
|
|
312
|
+
// 1. Check models explicitly listed inside the group
|
|
313
|
+
for (const m of group.models ?? []) {
|
|
314
|
+
const mid = (m.id ?? "").trim().toLowerCase();
|
|
315
|
+
if (!mid) continue;
|
|
316
|
+
|
|
317
|
+
for (const targetId of configuredModelIds) {
|
|
318
|
+
const tid = targetId.trim().toLowerCase();
|
|
319
|
+
if (mid === tid || tid.includes(mid) || mid.includes(tid)) return true;
|
|
320
|
+
|
|
321
|
+
const mTokens = tokenizeModelId(mid);
|
|
322
|
+
const overlap = mTokens.filter((t) => targetTokens.has(t));
|
|
323
|
+
|
|
324
|
+
// Guard: If group model has 'pro' but target only has 'flash', skip unless other strong overlap
|
|
325
|
+
const mHasPro = mTokens.includes("pro");
|
|
326
|
+
const tHasPro = tokenizeModelId(tid).includes("pro");
|
|
327
|
+
if (mHasPro !== tHasPro && (mTokens.includes("flash") || tokenizeModelId(tid).includes("flash"))) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (overlap.length >= 2 || overlap.some((t) => modelKeywords.includes(t) && !tierTokens.includes(t))) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 2. Check group label and id
|
|
338
|
+
const gTokens = tokenizeModelId(`${group.id ?? ""} ${group.label ?? ""}`);
|
|
339
|
+
|
|
340
|
+
// Distinct tier check: if group says 'pro' but user configured models don't have 'pro', reject
|
|
341
|
+
for (const tier of tierTokens) {
|
|
342
|
+
if (gTokens.includes(tier)) {
|
|
343
|
+
if (targetTokens.has(tier)) return true;
|
|
344
|
+
// Group has this tier keyword, but user configured models don't
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// General model token overlap
|
|
350
|
+
const gOverlap = gTokens.filter((t) => targetTokens.has(t));
|
|
351
|
+
return gOverlap.length >= 2 || gOverlap.some((t) => modelKeywords.includes(t));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolves a friendly, high-level group label based on the models contained
|
|
356
|
+
* inside this quota group.
|
|
357
|
+
* E.g., if a group contains 'gemini-2.5-pro' and 'gemini-3.1-pro', label it "Gemini".
|
|
358
|
+
* If it contains 'claude-opus-4-6-thinking', 'claude-sonnet-4-6', and 'gpt-oss-120b-medium',
|
|
359
|
+
* label it "Claude / GPT".
|
|
360
|
+
*/
|
|
361
|
+
export function resolveGroupFamilyLabel(group: RawBridgeGroup): string {
|
|
362
|
+
const models = group.models ?? [];
|
|
363
|
+
const families = new Set<string>();
|
|
364
|
+
|
|
365
|
+
for (const m of models) {
|
|
366
|
+
const text = `${m.id ?? ""} ${m.displayName ?? ""}`.toLowerCase();
|
|
367
|
+
if (text.includes("gemini")) families.add("Gemini");
|
|
368
|
+
else if (text.includes("claude")) families.add("Claude");
|
|
369
|
+
else if (text.includes("gpt") || text.includes("openai")) families.add("GPT");
|
|
370
|
+
else if (text.includes("codex")) families.add("Codex");
|
|
371
|
+
else if (text.includes("deepseek")) families.add("DeepSeek");
|
|
372
|
+
else if (text.includes("kimi") || text.includes("moonshot")) families.add("Kimi");
|
|
373
|
+
else if (text.includes("grok") || text.includes("xai")) families.add("Grok");
|
|
374
|
+
else if (text.includes("glm") || text.includes("zhipu")) families.add("GLM");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Also check the group id and label if models didn't provide family clues
|
|
378
|
+
const gText = `${group.id ?? ""} ${group.label ?? ""}`.toLowerCase();
|
|
379
|
+
if (gText.includes("gemini")) families.add("Gemini");
|
|
380
|
+
if (gText.includes("claude")) families.add("Claude");
|
|
381
|
+
if (gText.includes("gpt")) families.add("GPT");
|
|
382
|
+
if (gText.includes("codex")) families.add("Codex");
|
|
383
|
+
if (gText.includes("pro") || gText.includes("flash")) {
|
|
384
|
+
if (!families.has("Claude") && !families.has("GPT") && !families.has("Codex")) {
|
|
385
|
+
families.add("Gemini");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (gText.includes("thinking") || gText.includes("other")) {
|
|
389
|
+
if (!families.has("Gemini")) {
|
|
390
|
+
families.add("Claude");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (families.size > 0) {
|
|
395
|
+
const order = ["Gemini", "Claude", "GPT", "Codex", "DeepSeek", "Kimi", "Grok", "GLM"];
|
|
396
|
+
const sorted = order.filter((f) => families.has(f));
|
|
397
|
+
return sorted.join(" / ");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return group.label || group.id || "Quota";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Deduplicates groups in the same account that share the exact same quota pool
|
|
405
|
+
* (identical remaining fraction and identical reset time),
|
|
406
|
+
* and computes clean, friendly family labels (e.g. "Gemini", "Claude / GPT").
|
|
407
|
+
*/
|
|
408
|
+
export function deduplicateSharedQuotaGroups(groups: RawBridgeGroup[]): RawBridgeGroup[] {
|
|
409
|
+
const result: RawBridgeGroup[] = [];
|
|
410
|
+
const visited = new Set<number>();
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < groups.length; i++) {
|
|
413
|
+
if (visited.has(i)) continue;
|
|
414
|
+
const current = groups[i]!;
|
|
415
|
+
const matchingIndices: number[] = [i];
|
|
416
|
+
|
|
417
|
+
for (let j = i + 1; j < groups.length; j++) {
|
|
418
|
+
if (visited.has(j)) continue;
|
|
419
|
+
const other = groups[j]!;
|
|
420
|
+
|
|
421
|
+
// Compare remainingFraction and resetTime
|
|
422
|
+
if (
|
|
423
|
+
typeof current.remainingFraction === "number" &&
|
|
424
|
+
typeof other.remainingFraction === "number" &&
|
|
425
|
+
Math.abs(current.remainingFraction - other.remainingFraction) < 0.0001 &&
|
|
426
|
+
current.resetTime === other.resetTime
|
|
427
|
+
) {
|
|
428
|
+
matchingIndices.push(j);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (matchingIndices.length === 1) {
|
|
433
|
+
result.push({
|
|
434
|
+
...current,
|
|
435
|
+
label: resolveGroupFamilyLabel(current),
|
|
436
|
+
});
|
|
437
|
+
visited.add(i);
|
|
438
|
+
} else {
|
|
439
|
+
const matchedGroups = matchingIndices.map((idx) => groups[idx]!);
|
|
440
|
+
for (const idx of matchingIndices) visited.add(idx);
|
|
441
|
+
|
|
442
|
+
const allModels = matchedGroups.flatMap((g) => g.models ?? []);
|
|
443
|
+
const mergedGroup: RawBridgeGroup = {
|
|
444
|
+
...current,
|
|
445
|
+
id: matchedGroups.map((g) => g.id).filter(Boolean).join("+"),
|
|
446
|
+
models: allModels,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
result.push({
|
|
450
|
+
...mergedGroup,
|
|
451
|
+
label: resolveGroupFamilyLabel(mergedGroup),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|