@oh-my-pi/pi-ai 17.2.11 → 17.2.13
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/CHANGELOG.md +25 -0
- package/dist/types/auth-retry.d.ts +3 -3
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/error/auth-classify.d.ts +5 -5
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/error/flags.d.ts +4 -0
- package/dist/types/providers/aws-credentials.d.ts +4 -3
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +14 -0
- package/dist/types/providers/openai-shared.d.ts +9 -1
- package/dist/types/registry/api-key-login.d.ts +6 -4
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +2 -2
- package/dist/types/registry/kagi.d.ts +2 -2
- package/dist/types/registry/litellm.d.ts +2 -2
- package/dist/types/registry/llama-cpp.d.ts +2 -2
- package/dist/types/registry/lm-studio.d.ts +2 -2
- package/dist/types/registry/parallel.d.ts +2 -2
- package/dist/types/registry/tavily.d.ts +1 -1
- package/dist/types/registry/vercel-ai-gateway.d.ts +2 -2
- package/dist/types/registry/vllm.d.ts +2 -2
- package/dist/types/types.d.ts +7 -0
- package/dist/types/usage/cursor.d.ts +11 -0
- package/dist/types/usage/openai-codex-reset.d.ts +0 -20
- package/dist/types/usage/shared.d.ts +13 -1
- package/dist/types/utils/block-symbols.d.ts +12 -0
- package/package.json +5 -5
- package/src/auth-retry.ts +5 -4
- package/src/auth-storage.ts +77 -33
- package/src/dialect/owned-stream.ts +3 -0
- package/src/error/auth-classify.ts +7 -6
- package/src/error/aws.ts +5 -1
- package/src/error/flags.ts +14 -0
- package/src/providers/amazon-bedrock.ts +38 -0
- package/src/providers/aws-credentials.ts +222 -29
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +22 -0
- package/src/providers/cursor.ts +81 -1
- package/src/providers/google-gemini-cli.ts +49 -15
- package/src/providers/google-shared.ts +7 -1
- package/src/providers/openai-codex/request-transformer.ts +38 -17
- package/src/providers/openai-codex-responses.ts +2 -3
- package/src/providers/openai-responses.ts +4 -0
- package/src/providers/openai-shared.ts +55 -1
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/register-builtins.ts +18 -14
- package/src/registry/api-key-login.ts +26 -12
- package/src/registry/aws.ts +13 -6
- package/src/registry/cloudflare-ai-gateway.ts +10 -29
- package/src/registry/kagi.ts +11 -29
- package/src/registry/litellm.ts +11 -29
- package/src/registry/llama-cpp.ts +11 -21
- package/src/registry/lm-studio.ts +9 -20
- package/src/registry/oauth/callback-server.ts +93 -5
- package/src/registry/parallel.ts +10 -28
- package/src/registry/tavily.ts +9 -27
- package/src/registry/vercel-ai-gateway.ts +10 -28
- package/src/registry/vllm.ts +11 -21
- package/src/stream.ts +10 -7
- package/src/types.ts +7 -0
- package/src/usage/alibaba-token-plan.ts +8 -15
- package/src/usage/claude.ts +12 -19
- package/src/usage/cursor.ts +179 -59
- package/src/usage/gemini.ts +3 -2
- package/src/usage/github-copilot.ts +3 -2
- package/src/usage/google-antigravity.ts +10 -17
- package/src/usage/kimi.ts +35 -17
- package/src/usage/minimax-code.ts +7 -27
- package/src/usage/openai-codex-reset.ts +3 -2
- package/src/usage/openai-codex.ts +5 -5
- package/src/usage/opencode-go.ts +1 -2
- package/src/usage/shared.ts +33 -10
- package/src/usage/synthetic.ts +4 -11
- package/src/usage/umans.ts +2 -2
- package/src/usage/xai-oauth.ts +13 -26
- package/src/usage/zai.ts +4 -6
- package/src/utils/aws-profile.ts +39 -1
- package/src/utils/block-symbols.ts +18 -0
- package/src/utils/leaked-thinking-stream.ts +3 -0
- package/src/utils/openrouter-headers.ts +3 -3
package/src/usage/claude.ts
CHANGED
|
@@ -17,10 +17,9 @@ import {
|
|
|
17
17
|
type UsageWindow,
|
|
18
18
|
} from "../usage";
|
|
19
19
|
import { isRecord } from "../utils";
|
|
20
|
+
import { HOUR_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
|
|
20
21
|
|
|
21
22
|
const DEFAULT_ENDPOINT = "https://api.anthropic.com/api/oauth";
|
|
22
|
-
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
23
|
-
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
23
|
const MAX_ATTEMPTS = 3;
|
|
25
24
|
const BASE_RETRY_DELAY_MS = 500;
|
|
26
25
|
|
|
@@ -123,16 +122,10 @@ interface ParsedApiLimitEntry {
|
|
|
123
122
|
displayName?: string;
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
function parseIsoTime(value: string | undefined): number | undefined {
|
|
127
|
-
if (!value) return undefined;
|
|
128
|
-
const parsed = Date.parse(value);
|
|
129
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
125
|
function parseBucket(bucket: unknown): ParsedUsageBucket | undefined {
|
|
133
126
|
if (!isRecord(bucket)) return undefined;
|
|
134
127
|
const utilization = toNumber(bucket.utilization);
|
|
135
|
-
const resetsAt =
|
|
128
|
+
const resetsAt = parseIsoTimestamp(typeof bucket.resets_at === "string" ? bucket.resets_at : undefined);
|
|
136
129
|
if (utilization === undefined && resetsAt === undefined) {
|
|
137
130
|
return undefined;
|
|
138
131
|
}
|
|
@@ -168,7 +161,7 @@ function parseApiLimitEntries(raw: unknown): ParsedApiLimitEntry[] {
|
|
|
168
161
|
const entry = rawEntry as ClaudeApiLimitEntry;
|
|
169
162
|
if (typeof entry.kind !== "string") continue;
|
|
170
163
|
const utilization = toNumber(entry.percent);
|
|
171
|
-
const resetsAt =
|
|
164
|
+
const resetsAt = parseIsoTimestamp(typeof entry.resets_at === "string" ? entry.resets_at : undefined);
|
|
172
165
|
if (utilization === undefined && resetsAt === undefined) continue;
|
|
173
166
|
const displayName = getApiLimitDisplayName(entry.scope);
|
|
174
167
|
entries.push({
|
|
@@ -554,7 +547,7 @@ function buildScopedWeeklyUsageLimits(entries: readonly ParsedApiLimitEntry[]):
|
|
|
554
547
|
label: `Claude 7 Day (${entry.displayName})`,
|
|
555
548
|
windowId: "7d",
|
|
556
549
|
windowLabel: "7 Day",
|
|
557
|
-
durationMs:
|
|
550
|
+
durationMs: WEEK_MS,
|
|
558
551
|
bucket: entry.bucket,
|
|
559
552
|
provider: "anthropic",
|
|
560
553
|
tier: slug,
|
|
@@ -574,7 +567,7 @@ export function parseClaudeRateLimitHeaders(headers: Record<string, string>, now
|
|
|
574
567
|
label: "Claude 5 Hour",
|
|
575
568
|
windowId: "5h",
|
|
576
569
|
windowLabel: "5 Hour",
|
|
577
|
-
durationMs:
|
|
570
|
+
durationMs: 5 * HOUR_MS,
|
|
578
571
|
bucket: fiveHour,
|
|
579
572
|
provider: "anthropic",
|
|
580
573
|
shared: true,
|
|
@@ -584,7 +577,7 @@ export function parseClaudeRateLimitHeaders(headers: Record<string, string>, now
|
|
|
584
577
|
label: "Claude 7 Day",
|
|
585
578
|
windowId: "7d",
|
|
586
579
|
windowLabel: "7 Day",
|
|
587
|
-
durationMs:
|
|
580
|
+
durationMs: WEEK_MS,
|
|
588
581
|
bucket: sevenDay,
|
|
589
582
|
provider: "anthropic",
|
|
590
583
|
shared: true,
|
|
@@ -594,7 +587,7 @@ export function parseClaudeRateLimitHeaders(headers: Record<string, string>, now
|
|
|
594
587
|
label: "Claude 7 Day (Fable)",
|
|
595
588
|
windowId: "7d",
|
|
596
589
|
windowLabel: "7 Day",
|
|
597
|
-
durationMs:
|
|
590
|
+
durationMs: WEEK_MS,
|
|
598
591
|
bucket: modelScopedSevenDay,
|
|
599
592
|
provider: "anthropic",
|
|
600
593
|
tier: "fable",
|
|
@@ -638,7 +631,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
638
631
|
label: "Claude 5 Hour",
|
|
639
632
|
windowId: "5h",
|
|
640
633
|
windowLabel: "5 Hour",
|
|
641
|
-
durationMs:
|
|
634
|
+
durationMs: 5 * HOUR_MS,
|
|
642
635
|
bucket: fiveHour,
|
|
643
636
|
provider: "anthropic",
|
|
644
637
|
shared: true,
|
|
@@ -648,7 +641,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
648
641
|
label: "Claude 7 Day",
|
|
649
642
|
windowId: "7d",
|
|
650
643
|
windowLabel: "7 Day",
|
|
651
|
-
durationMs:
|
|
644
|
+
durationMs: WEEK_MS,
|
|
652
645
|
bucket: sevenDay,
|
|
653
646
|
provider: "anthropic",
|
|
654
647
|
shared: true,
|
|
@@ -658,7 +651,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
658
651
|
label: "Claude 7 Day (Opus)",
|
|
659
652
|
windowId: "7d",
|
|
660
653
|
windowLabel: "7 Day",
|
|
661
|
-
durationMs:
|
|
654
|
+
durationMs: WEEK_MS,
|
|
662
655
|
bucket: sevenDayOpus,
|
|
663
656
|
provider: "anthropic",
|
|
664
657
|
tier: "opus",
|
|
@@ -668,7 +661,7 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
668
661
|
label: "Claude 7 Day (Sonnet)",
|
|
669
662
|
windowId: "7d",
|
|
670
663
|
windowLabel: "7 Day",
|
|
671
|
-
durationMs:
|
|
664
|
+
durationMs: WEEK_MS,
|
|
672
665
|
bucket: sevenDaySonnet,
|
|
673
666
|
provider: "anthropic",
|
|
674
667
|
tier: "sonnet",
|
|
@@ -774,7 +767,7 @@ function rankingUsedFraction(limit: UsageLimit): number {
|
|
|
774
767
|
|
|
775
768
|
function rankingDrainRate(limit: UsageLimit, nowMs: number): number {
|
|
776
769
|
const usedFraction = rankingUsedFraction(limit);
|
|
777
|
-
const durationMs = limit.window?.durationMs ??
|
|
770
|
+
const durationMs = limit.window?.durationMs ?? WEEK_MS;
|
|
778
771
|
if (!Number.isFinite(durationMs) || durationMs <= 0) return usedFraction;
|
|
779
772
|
const resetAt = limit.window?.resetsAt;
|
|
780
773
|
if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return usedFraction;
|
package/src/usage/cursor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
1
2
|
import { extractCursorAccessTokenUserId } from "../registry/oauth/cursor";
|
|
2
3
|
import type {
|
|
3
4
|
UsageAmount,
|
|
@@ -6,21 +7,15 @@ import type {
|
|
|
6
7
|
UsageLimit,
|
|
7
8
|
UsageProvider,
|
|
8
9
|
UsageReport,
|
|
9
|
-
UsageStatus,
|
|
10
10
|
UsageWindow,
|
|
11
11
|
} from "../usage";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
-
}
|
|
12
|
+
import { isRecord } from "../utils";
|
|
13
|
+
import { parseIsoTimestamp, usageStatus } from "./shared";
|
|
17
14
|
|
|
18
15
|
function parseTimestamp(value: unknown): number | undefined {
|
|
19
16
|
const numeric = toNumber(value);
|
|
20
17
|
if (numeric !== undefined) return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
|
|
21
|
-
|
|
22
|
-
const parsed = Date.parse(value);
|
|
23
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
18
|
+
return parseIsoTimestamp(value);
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
const DEFAULT_CURSOR_BASE_URL = "https://api2.cursor.sh";
|
|
@@ -78,51 +73,145 @@ function deriveResetsAt(payload: Record<string, unknown>): number | undefined {
|
|
|
78
73
|
return undefined;
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
export function parseCursorIndividualUsage(payload: unknown, fetchedAt = Date.now()): UsageReport | null {
|
|
89
|
-
if (!isRecord(payload) || !isRecord(payload.individualUsage) || !isRecord(payload.individualUsage.overall)) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
const individual = payload.individualUsage.overall;
|
|
93
|
-
if (individual.enabled === false) return null;
|
|
76
|
+
/**
|
|
77
|
+
* Parse a Cursor cents bucket (`used`/`limit`/`remaining` in USD cents).
|
|
78
|
+
* Returns null for disabled or non-positive / malformed buckets.
|
|
79
|
+
*/
|
|
80
|
+
function parseCursorCentsBucket(bucket: Record<string, unknown>): UsageAmount | null {
|
|
81
|
+
if (bucket.enabled === false) return null;
|
|
94
82
|
|
|
95
|
-
const reportedUsed = toNumber(
|
|
96
|
-
const reportedRemaining = toNumber(
|
|
83
|
+
const reportedUsed = toNumber(bucket.used);
|
|
84
|
+
const reportedRemaining = toNumber(bucket.remaining);
|
|
97
85
|
const hasValidUsed = reportedUsed !== undefined && reportedUsed >= 0;
|
|
98
86
|
const hasValidRemaining = reportedRemaining !== undefined && reportedRemaining >= 0;
|
|
99
|
-
const limit = toNumber(
|
|
87
|
+
const limit = toNumber(bucket.limit);
|
|
100
88
|
|
|
101
|
-
|
|
102
|
-
if (individual.limit === null || individual.limit === undefined) {
|
|
89
|
+
if (bucket.limit === null || bucket.limit === undefined) {
|
|
103
90
|
if (!hasValidUsed) return null;
|
|
104
|
-
|
|
91
|
+
return { used: reportedUsed / 100, unit: "usd" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (limit === undefined || limit <= 0) return null;
|
|
95
|
+
let used: number;
|
|
96
|
+
if (reportedUsed !== undefined && reportedUsed > 0) {
|
|
97
|
+
used = reportedUsed;
|
|
98
|
+
} else if (hasValidRemaining && reportedRemaining < limit) {
|
|
99
|
+
used = Math.max(0, limit - reportedRemaining);
|
|
100
|
+
} else if (hasValidUsed) {
|
|
101
|
+
used = reportedUsed;
|
|
105
102
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const remaining = Math.max(0, limit - used);
|
|
106
|
+
return {
|
|
107
|
+
used: used / 100,
|
|
108
|
+
limit: limit / 100,
|
|
109
|
+
remaining: remaining / 100,
|
|
110
|
+
usedFraction: used / limit,
|
|
111
|
+
remainingFraction: remaining / limit,
|
|
112
|
+
unit: "usd",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Cursor's dashboard does not treat plan.used / plan.limit as the visible %.
|
|
118
|
+
* Pro+ shows separate quota pools (not one shared percent):
|
|
119
|
+
* - Cursor Models ← autoPercentUsed
|
|
120
|
+
* (includes Cursor Grok 4.5 and Composer 2.5)
|
|
121
|
+
* - Other Models ← apiPercentUsed (separate included-$ pool; different quota)
|
|
122
|
+
* Prefer those fractions when present; fall back to cents only for older overall buckets.
|
|
123
|
+
*/
|
|
124
|
+
function parseCursorPlanDashboardAmounts(bucket: Record<string, unknown>): {
|
|
125
|
+
auto?: UsageAmount;
|
|
126
|
+
api?: UsageAmount;
|
|
127
|
+
fallback?: UsageAmount;
|
|
128
|
+
} {
|
|
129
|
+
if (bucket.enabled === false) return {};
|
|
130
|
+
|
|
131
|
+
const limitCents = toNumber(bucket.limit);
|
|
132
|
+
const limitUsd = limitCents !== undefined && limitCents > 0 ? limitCents / 100 : undefined;
|
|
133
|
+
const autoPct = toNumber(bucket.autoPercentUsed);
|
|
134
|
+
const apiPct = toNumber(bucket.apiPercentUsed);
|
|
135
|
+
const totalPct = toNumber(bucket.totalPercentUsed);
|
|
136
|
+
|
|
137
|
+
const fromPercent = (pct: number, withLimit: boolean): UsageAmount => {
|
|
138
|
+
const usedFraction = Math.max(0, pct) / 100;
|
|
139
|
+
if (withLimit && limitUsd !== undefined) {
|
|
140
|
+
const used = limitUsd * usedFraction;
|
|
141
|
+
return {
|
|
142
|
+
used,
|
|
143
|
+
limit: limitUsd,
|
|
144
|
+
remaining: Math.max(0, limitUsd - used),
|
|
145
|
+
usedFraction,
|
|
146
|
+
remainingFraction: Math.max(0, 1 - usedFraction),
|
|
147
|
+
unit: "usd",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { used: usedFraction * 100, usedFraction, unit: "percent" };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const result: { auto?: UsageAmount; api?: UsageAmount; fallback?: UsageAmount } = {};
|
|
154
|
+
if (autoPct !== undefined) result.auto = fromPercent(autoPct, false);
|
|
155
|
+
if (apiPct !== undefined) result.api = fromPercent(apiPct, true);
|
|
156
|
+
if (!result.auto && !result.api) {
|
|
157
|
+
if (totalPct !== undefined) {
|
|
158
|
+
result.fallback = fromPercent(totalPct, true);
|
|
114
159
|
} else {
|
|
115
|
-
|
|
160
|
+
const cents = parseCursorCentsBucket(bucket);
|
|
161
|
+
if (cents) result.fallback = cents;
|
|
116
162
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function pushCursorPlanRails(limits: UsageLimit[], bucket: Record<string, unknown>, window: UsageWindow): void {
|
|
168
|
+
const rails = parseCursorPlanDashboardAmounts(bucket);
|
|
169
|
+
if (rails.auto) {
|
|
170
|
+
limits.push({
|
|
171
|
+
id: "cursor:usd:individual-auto",
|
|
172
|
+
label: "Cursor Models",
|
|
173
|
+
scope: { provider: "cursor", windowId: window.id },
|
|
174
|
+
window,
|
|
175
|
+
amount: rails.auto,
|
|
176
|
+
...(rails.auto.usedFraction !== undefined ? { status: usageStatus(rails.auto.usedFraction) } : {}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (rails.api) {
|
|
180
|
+
limits.push({
|
|
181
|
+
id: "cursor:usd:individual-api",
|
|
182
|
+
label: "Other Models",
|
|
183
|
+
scope: { provider: "cursor", windowId: window.id },
|
|
184
|
+
window,
|
|
185
|
+
amount: rails.api,
|
|
186
|
+
...(rails.api.usedFraction !== undefined ? { status: usageStatus(rails.api.usedFraction) } : {}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (rails.fallback) {
|
|
190
|
+
limits.push({
|
|
191
|
+
id: "cursor:usd:individual-plan",
|
|
192
|
+
label: "Personal Usage",
|
|
193
|
+
scope: { provider: "cursor", windowId: window.id },
|
|
194
|
+
window,
|
|
195
|
+
amount: rails.fallback,
|
|
196
|
+
...(rails.fallback.usedFraction !== undefined ? { status: usageStatus(rails.fallback.usedFraction) } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Cursor's `/api/usage-summary` has shipped two personal-bucket shapes:
|
|
203
|
+
* - Enterprise/team dashboards historically exposed `individualUsage.overall`
|
|
204
|
+
* - Current Pro / Pro+ / Ultra dashboards expose `individualUsage.plan`
|
|
205
|
+
* (plus optional `onDemand`)
|
|
206
|
+
*
|
|
207
|
+
* Prefer a *usable* overall bucket; if overall is absent/disabled/malformed,
|
|
208
|
+
* fall through to plan rails (`autoPercentUsed` / `apiPercentUsed`). Always
|
|
209
|
+
* consider on-demand afterward so a valid on-demand meter is not dropped when
|
|
210
|
+
* the included plan bucket is empty.
|
|
211
|
+
*/
|
|
212
|
+
export function parseCursorIndividualUsage(payload: unknown, fetchedAt = Date.now()): UsageReport | null {
|
|
213
|
+
if (!isRecord(payload) || !isRecord(payload.individualUsage)) {
|
|
214
|
+
return null;
|
|
126
215
|
}
|
|
127
216
|
|
|
128
217
|
const resetsAt = deriveResetsAt(payload);
|
|
@@ -131,21 +220,52 @@ export function parseCursorIndividualUsage(payload: unknown, fetchedAt = Date.no
|
|
|
131
220
|
label: "Monthly",
|
|
132
221
|
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
133
222
|
};
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
amount
|
|
143
|
-
|
|
144
|
-
|
|
223
|
+
const limits: UsageLimit[] = [];
|
|
224
|
+
|
|
225
|
+
const overall = isRecord(payload.individualUsage.overall) ? payload.individualUsage.overall : null;
|
|
226
|
+
const plan = isRecord(payload.individualUsage.plan) ? payload.individualUsage.plan : null;
|
|
227
|
+
|
|
228
|
+
// Prefer a usable overall bucket; if it is disabled/malformed, fall through to plan.
|
|
229
|
+
let usedOverall = false;
|
|
230
|
+
if (overall) {
|
|
231
|
+
const amount = parseCursorCentsBucket(overall);
|
|
232
|
+
if (amount) {
|
|
233
|
+
usedOverall = true;
|
|
234
|
+
limits.push({
|
|
235
|
+
id: "cursor:usd:individual-overall",
|
|
236
|
+
label: "Personal Usage",
|
|
237
|
+
scope: { provider: "cursor", windowId: window.id },
|
|
238
|
+
window,
|
|
239
|
+
amount,
|
|
240
|
+
...(amount.usedFraction !== undefined ? { status: usageStatus(amount.usedFraction) } : {}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!usedOverall && plan) {
|
|
245
|
+
pushCursorPlanRails(limits, plan, window);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Keep on-demand even when the included plan/overall bucket is absent or unusable.
|
|
249
|
+
if (isRecord(payload.individualUsage.onDemand)) {
|
|
250
|
+
const onDemandAmount = parseCursorCentsBucket(payload.individualUsage.onDemand);
|
|
251
|
+
if (onDemandAmount && onDemandAmount.limit !== undefined && onDemandAmount.limit > 0) {
|
|
252
|
+
limits.push({
|
|
253
|
+
id: "cursor:usd:individual-ondemand",
|
|
254
|
+
label: "On-Demand Usage",
|
|
255
|
+
scope: { provider: "cursor", windowId: window.id },
|
|
256
|
+
window,
|
|
257
|
+
amount: onDemandAmount,
|
|
258
|
+
...(onDemandAmount.usedFraction !== undefined ? { status: usageStatus(onDemandAmount.usedFraction) } : {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (limits.length === 0) return null;
|
|
264
|
+
|
|
145
265
|
return {
|
|
146
266
|
provider: "cursor",
|
|
147
267
|
fetchedAt,
|
|
148
|
-
limits
|
|
268
|
+
limits,
|
|
149
269
|
raw: payload,
|
|
150
270
|
};
|
|
151
271
|
}
|
|
@@ -197,7 +317,7 @@ export function parseCursorUsage(payload: unknown, fetchedAt = Date.now()): Usag
|
|
|
197
317
|
unit,
|
|
198
318
|
};
|
|
199
319
|
|
|
200
|
-
const status =
|
|
320
|
+
const status = usageStatus(amount.usedFraction);
|
|
201
321
|
|
|
202
322
|
limits.push({
|
|
203
323
|
id: limitId,
|
package/src/usage/gemini.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
UsageReport,
|
|
9
9
|
UsageWindow,
|
|
10
10
|
} from "../usage";
|
|
11
|
+
import { parseIsoTimestamp } from "./shared";
|
|
11
12
|
|
|
12
13
|
// (Refresh is the sole responsibility of AuthStorage; no provider-direct refresh here.)
|
|
13
14
|
|
|
@@ -79,8 +80,8 @@ function parseWindow(resetTime: string | undefined): UsageWindow {
|
|
|
79
80
|
label: "Quota window",
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
|
-
const resetsAt =
|
|
83
|
-
if (
|
|
83
|
+
const resetsAt = parseIsoTimestamp(resetTime);
|
|
84
|
+
if (resetsAt === undefined) {
|
|
84
85
|
return {
|
|
85
86
|
id: "quota",
|
|
86
87
|
label: "Quota window",
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
UsageWindow,
|
|
19
19
|
} from "../usage";
|
|
20
20
|
import { isRecord } from "../utils";
|
|
21
|
+
import { parseIsoTimestamp } from "./shared";
|
|
21
22
|
|
|
22
23
|
type CopilotQuotaDetail = {
|
|
23
24
|
entitlement: number;
|
|
@@ -74,8 +75,8 @@ function resolveGitHubApiBaseUrl(params: UsageFetchParams): string {
|
|
|
74
75
|
|
|
75
76
|
function buildWindow(resetDate: string | undefined): UsageWindow | undefined {
|
|
76
77
|
if (!resetDate) return undefined;
|
|
77
|
-
const resetAt =
|
|
78
|
-
if (
|
|
78
|
+
const resetAt = parseIsoTimestamp(resetDate);
|
|
79
|
+
if (resetAt === undefined) return undefined;
|
|
79
80
|
return {
|
|
80
81
|
id: "monthly",
|
|
81
82
|
label: "Monthly",
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
UsageStatus,
|
|
13
13
|
UsageWindow,
|
|
14
14
|
} from "../usage";
|
|
15
|
+
import { DAY_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
|
|
15
16
|
|
|
16
17
|
// (Refresh is the sole responsibility of AuthStorage; no provider-direct refresh here.)
|
|
17
18
|
|
|
@@ -47,9 +48,6 @@ interface AntigravityUsageResponse {
|
|
|
47
48
|
const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
48
49
|
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
|
49
50
|
|
|
50
|
-
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
51
|
-
const ONE_WEEK_MS = 7 * ONE_DAY_MS;
|
|
52
|
-
|
|
53
51
|
interface AntigravityWindowDescriptor {
|
|
54
52
|
id: string;
|
|
55
53
|
label: string;
|
|
@@ -59,25 +57,20 @@ interface AntigravityWindowDescriptor {
|
|
|
59
57
|
function classifyWindow(id: string | undefined, label: string | undefined): AntigravityWindowDescriptor | undefined {
|
|
60
58
|
const source = `${id ?? ""} ${label ?? ""}`.toLowerCase();
|
|
61
59
|
if (source.includes("week") || source.includes("7d") || /7[\s_-]*day/.test(source)) {
|
|
62
|
-
return { id: "weekly", label: "Weekly", durationMs:
|
|
60
|
+
return { id: "weekly", label: "Weekly", durationMs: WEEK_MS };
|
|
63
61
|
}
|
|
64
62
|
if (source.includes("day") || source.includes("daily") || source.includes("24h")) {
|
|
65
|
-
return { id: "daily", label: "Daily", durationMs:
|
|
63
|
+
return { id: "daily", label: "Daily", durationMs: DAY_MS };
|
|
66
64
|
}
|
|
67
65
|
if (id || label) return { id: id ?? label ?? "default", label: label ?? id ?? "Default" };
|
|
68
66
|
return undefined;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
function parseResetTime(info: AntigravityQuotaInfo): number | undefined {
|
|
72
|
-
const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
|
|
73
|
-
return resetAt !== undefined && Number.isFinite(resetAt) ? resetAt : undefined;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
69
|
function inferWindowFromReset(resetAt: number | undefined, nowMs: number): AntigravityWindowDescriptor {
|
|
77
|
-
if (resetAt !== undefined && resetAt - nowMs >
|
|
78
|
-
return { id: "weekly", label: "Weekly", durationMs:
|
|
70
|
+
if (resetAt !== undefined && resetAt - nowMs > DAY_MS) {
|
|
71
|
+
return { id: "weekly", label: "Weekly", durationMs: WEEK_MS };
|
|
79
72
|
}
|
|
80
|
-
return { id: "daily", label: "Daily", durationMs:
|
|
73
|
+
return { id: "daily", label: "Daily", durationMs: DAY_MS };
|
|
81
74
|
}
|
|
82
75
|
|
|
83
76
|
function quotaInferenceKey(info: AntigravityQuotaInfo): string {
|
|
@@ -98,7 +91,7 @@ function inferWindowDescriptors(
|
|
|
98
91
|
continue;
|
|
99
92
|
}
|
|
100
93
|
const group = groups.get(quotaInferenceKey(info)) ?? [];
|
|
101
|
-
group.push({ info, resetAt:
|
|
94
|
+
group.push({ info, resetAt: parseIsoTimestamp(info.resetTime) });
|
|
102
95
|
groups.set(quotaInferenceKey(info), group);
|
|
103
96
|
}
|
|
104
97
|
|
|
@@ -110,7 +103,7 @@ function inferWindowDescriptors(
|
|
|
110
103
|
for (const entry of group) {
|
|
111
104
|
const descriptor =
|
|
112
105
|
latestReset !== undefined && entry.resetAt === latestReset
|
|
113
|
-
? { id: "weekly", label: "Weekly", durationMs:
|
|
106
|
+
? { id: "weekly", label: "Weekly", durationMs: WEEK_MS }
|
|
114
107
|
: inferWindowFromReset(entry.resetAt, nowMs);
|
|
115
108
|
descriptors.set(entry.info, descriptor);
|
|
116
109
|
}
|
|
@@ -149,7 +142,7 @@ function parseWindow(
|
|
|
149
142
|
info: AntigravityQuotaInfo,
|
|
150
143
|
descriptor: AntigravityWindowDescriptor | undefined,
|
|
151
144
|
): UsageWindow | undefined {
|
|
152
|
-
const resetAt =
|
|
145
|
+
const resetAt = parseIsoTimestamp(info.resetTime);
|
|
153
146
|
const hasResetAt = resetAt !== undefined;
|
|
154
147
|
if (!descriptor && !hasResetAt) return undefined;
|
|
155
148
|
return {
|
|
@@ -493,5 +486,5 @@ export const antigravityRankingStrategy: CredentialRankingStrategy = {
|
|
|
493
486
|
// Antigravity windows carry `durationMs` when the response identifies them
|
|
494
487
|
// as daily/weekly. Fall back to daily for legacy unlabelled quotaInfo
|
|
495
488
|
// entries from `daily-cloudcode-pa.googleapis.com`.
|
|
496
|
-
windowDefaults: { primaryMs:
|
|
489
|
+
windowDefaults: { primaryMs: DAY_MS, secondaryMs: DAY_MS },
|
|
497
490
|
};
|
package/src/usage/kimi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
1
2
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { getKimiCommonHeaders } from "../registry/oauth/kimi";
|
|
3
4
|
import type {
|
|
@@ -7,12 +8,12 @@ import type {
|
|
|
7
8
|
UsageLimit,
|
|
8
9
|
UsageProvider,
|
|
9
10
|
UsageReport,
|
|
10
|
-
UsageStatus,
|
|
11
11
|
UsageWindow,
|
|
12
12
|
} from "../usage";
|
|
13
13
|
import { isRecord } from "../utils";
|
|
14
|
+
import { parseIsoTimestamp, usageStatus } from "./shared";
|
|
15
|
+
|
|
14
16
|
// (Refresh is the sole responsibility of AuthStorage; no provider-direct refresh here.)
|
|
15
|
-
import { toNumber } from "./shared";
|
|
16
17
|
|
|
17
18
|
const DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
18
19
|
const USAGE_PATH = "usages";
|
|
@@ -47,8 +48,8 @@ function parseResetTime(data: Record<string, unknown>, nowMs: number): number |
|
|
|
47
48
|
for (const key of timeKeys) {
|
|
48
49
|
const value = data[key];
|
|
49
50
|
if (typeof value === "string" && value.trim()) {
|
|
50
|
-
const parsed =
|
|
51
|
-
if (
|
|
51
|
+
const parsed = parseIsoTimestamp(value);
|
|
52
|
+
if (parsed !== undefined) return parsed;
|
|
52
53
|
}
|
|
53
54
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
54
55
|
return value > 1_000_000_000_000 ? value : value * 1000;
|
|
@@ -76,6 +77,23 @@ function formatDurationLabel(duration: number, timeUnit: string): string | undef
|
|
|
76
77
|
return undefined;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
const MINUTE_MS = 60_000;
|
|
81
|
+
const HOUR_MS = 3_600_000;
|
|
82
|
+
const DAY_MS = 86_400_000;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Status-line and ranking consumers match on canonical window ids ("5h",
|
|
86
|
+
* "7d"), so derive the id from the reported span: the 300-minute burst window
|
|
87
|
+
* surfaces as "5h" instead of "300time_unit_minute". Mirrors the
|
|
88
|
+
* intervalWindowId convention in minimax-code.ts.
|
|
89
|
+
*/
|
|
90
|
+
function canonicalWindowId(durationMs: number): string {
|
|
91
|
+
if (durationMs > 0 && durationMs % DAY_MS === 0) return `${durationMs / DAY_MS}d`;
|
|
92
|
+
if (durationMs > 0 && durationMs % HOUR_MS === 0) return `${durationMs / HOUR_MS}h`;
|
|
93
|
+
const minutes = Math.round(durationMs / MINUTE_MS);
|
|
94
|
+
return minutes > 0 ? `${minutes}m` : "default";
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
function buildWindow(windowData: Record<string, unknown>, nowMs: number): UsageWindow | undefined {
|
|
80
98
|
const duration = toNumber(windowData.duration);
|
|
81
99
|
const timeUnit = typeof windowData.timeUnit === "string" ? windowData.timeUnit : "";
|
|
@@ -85,14 +103,15 @@ function buildWindow(windowData: Record<string, unknown>, nowMs: number): UsageW
|
|
|
85
103
|
if (duration === undefined && !label && !resetsAt) return undefined;
|
|
86
104
|
let durationMs: number | undefined;
|
|
87
105
|
if (duration !== undefined) {
|
|
88
|
-
if (timeUnit.toUpperCase().includes("MINUTE")) durationMs = duration *
|
|
89
|
-
else if (timeUnit.toUpperCase().includes("HOUR")) durationMs = duration *
|
|
90
|
-
else if (timeUnit.toUpperCase().includes("DAY")) durationMs = duration *
|
|
106
|
+
if (timeUnit.toUpperCase().includes("MINUTE")) durationMs = duration * MINUTE_MS;
|
|
107
|
+
else if (timeUnit.toUpperCase().includes("HOUR")) durationMs = duration * HOUR_MS;
|
|
108
|
+
else if (timeUnit.toUpperCase().includes("DAY")) durationMs = duration * DAY_MS;
|
|
109
|
+
else if (timeUnit.toUpperCase().includes("WEEK")) durationMs = duration * 7 * DAY_MS;
|
|
91
110
|
else if (timeUnit.toUpperCase().includes("SECOND")) durationMs = duration * 1000;
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
return {
|
|
95
|
-
id:
|
|
114
|
+
id: durationMs !== undefined ? canonicalWindowId(durationMs) : "default",
|
|
96
115
|
label: label ?? "Usage window",
|
|
97
116
|
durationMs,
|
|
98
117
|
resetsAt,
|
|
@@ -136,13 +155,6 @@ function buildUsageAmount(row: KimiUsageRow): UsageAmount {
|
|
|
136
155
|
return amount;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
function buildUsageStatus(amount: UsageAmount): UsageStatus {
|
|
140
|
-
if (amount.usedFraction === undefined) return "unknown";
|
|
141
|
-
if (amount.usedFraction >= 1) return "exhausted";
|
|
142
|
-
if (amount.usedFraction >= 0.9) return "warning";
|
|
143
|
-
return "ok";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
158
|
function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accountId?: string): UsageLimit {
|
|
147
159
|
// Kimi puts `resetTime` on the limit `detail`, not on `window`, so a
|
|
148
160
|
// window built from `duration`/`timeUnit` alone carries no resetsAt.
|
|
@@ -172,7 +184,7 @@ function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accoun
|
|
|
172
184
|
},
|
|
173
185
|
window,
|
|
174
186
|
amount,
|
|
175
|
-
status:
|
|
187
|
+
status: usageStatus(amount.usedFraction),
|
|
176
188
|
};
|
|
177
189
|
}
|
|
178
190
|
|
|
@@ -183,7 +195,13 @@ function parseUsagePayload(payload: unknown, nowMs: number): { rows: KimiUsageRo
|
|
|
183
195
|
|
|
184
196
|
if (isRecord(data.usage)) {
|
|
185
197
|
const summary = buildUsageRow(data.usage, "Total quota", nowMs);
|
|
186
|
-
if (summary)
|
|
198
|
+
if (summary) {
|
|
199
|
+
// Kimi Code's aggregate quota resets weekly, but the payload carries
|
|
200
|
+
// only `resetTime` and no duration. Attach the canonical weekly
|
|
201
|
+
// window explicitly so status-line/ranking consumers recognize it.
|
|
202
|
+
summary.window = { id: "7d", label: "7 Day", resetsAt: summary.resetsAt };
|
|
203
|
+
rows.push(summary);
|
|
204
|
+
}
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
if (Array.isArray(data.limits)) {
|