@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
  {
2
2
  "name": "@wayner6/pi-usage",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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 ?? []).map((account, index) => ({
68
- id: account.authIndex ?? `${account.provider ?? "provider"}-${index}`,
69
- provider: account.provider ?? "unknown",
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
- // Filter accounts based on user-configured models for this provider in Pi
80
- if (target.configuredModelIds && target.configuredModelIds.length > 0) {
81
- accounts = accounts.filter((account) => isAccountRelevantToModels(account, target.configuredModelIds));
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(ctx.modelRegistry.getAvailable().map((model) => model.provider));
79
- for (const id of ctx.modelRegistry.getRegisteredProviderIds()) providerIds.add(id);
80
- if (ctx.modelRegistry.getProviderAuthStatus("deepseek").configured) providerIds.add("deepseek");
81
- if (ctx.modelRegistry.getProviderAuthStatus("openai-codex").configured) providerIds.add("openai-codex");
82
- if (ctx.modelRegistry.getProviderAuthStatus("xai").configured) providerIds.add("xai");
83
- if (ctx.modelRegistry.getProviderAuthStatus("anthropic").configured) providerIds.add("anthropic");
84
- if (ctx.modelRegistry.getProviderAuthStatus("zai-coding-cn").configured) providerIds.add("zai-coding-cn");
85
- if (ctx.modelRegistry.getProviderAuthStatus("zai").configured) providerIds.add("zai");
86
- if (ctx.modelRegistry.getProviderAuthStatus("glm").configured) providerIds.add("glm");
87
- for (const id of Object.keys(this.config.providerOverrides)) providerIds.add(id);
88
- if (ctx.model) providerIds.add(ctx.model.provider);
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
+