@oh-my-pi/pi-ai 18.4.2 → 18.4.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/CHANGELOG.md +30 -41
- package/dist/types/auth-broker/protocol.d.ts +12 -0
- package/dist/types/dialect/rendering.d.ts +4 -0
- package/dist/types/images/openai-hosted.d.ts +1 -1
- package/dist/types/images/shared.d.ts +5 -2
- package/dist/types/providers/anthropic-wire.d.ts +9 -1
- package/dist/types/providers/anthropic.d.ts +17 -0
- package/dist/types/providers/aws-sigv4.d.ts +5 -0
- package/dist/types/providers/bedrock-anthropic.d.ts +9 -0
- package/dist/types/providers/bedrock-request-metadata.d.ts +2 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +10 -0
- package/dist/types/providers/openai-chat-wire.d.ts +2 -2
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-responses-wire.d.ts +2 -2
- package/dist/types/providers/xai-base-url.d.ts +17 -0
- package/dist/types/types.d.ts +14 -3
- package/dist/types/usage/commandcode.d.ts +4 -0
- package/dist/types/usage/shared.d.ts +13 -1
- package/dist/types/utils/event-stream.d.ts +7 -0
- package/dist/types/utils/openai-http.d.ts +7 -1
- package/package.json +6 -6
- package/src/auth-broker/client.ts +1 -12
- package/src/auth-broker/protocol.ts +32 -0
- package/src/auth-broker/remote-store.ts +4 -40
- package/src/auth-broker/server.ts +1 -20
- package/src/auth-broker/snapshot-cache.ts +1 -9
- package/src/dialect/anthropic.ts +3 -25
- package/src/dialect/minimax.ts +3 -24
- package/src/dialect/rendering.ts +18 -0
- package/src/dialect/thinking.ts +24 -6
- package/src/dialect/xml.ts +3 -19
- package/src/images/openai-hosted.ts +5 -2
- package/src/images/openai-images.ts +10 -4
- package/src/images/shared.ts +9 -4
- package/src/providers/amazon-bedrock.ts +61 -22
- package/src/providers/anthropic-compaction.ts +10 -1
- package/src/providers/anthropic-wire.ts +12 -1
- package/src/providers/anthropic.ts +58 -15
- package/src/providers/aws-eventstream.ts +4 -3
- package/src/providers/aws-sigv4.ts +1 -1
- package/src/providers/azure-openai-responses.ts +3 -4
- package/src/providers/bedrock-anthropic.ts +30 -0
- package/src/providers/bedrock-request-metadata.ts +6 -0
- package/src/providers/connect-error-detail.ts +1 -5
- package/src/providers/cursor/interaction-query.ts +4 -2
- package/src/providers/cursor.ts +30 -26
- package/src/providers/google-gemini-cli.ts +9 -2
- package/src/providers/google-shared.ts +16 -6
- package/src/providers/openai-chat-wire.ts +2 -2
- package/src/providers/openai-codex/request-transformer.ts +1 -1
- package/src/providers/openai-codex-responses.ts +27 -16
- package/src/providers/openai-completions.ts +51 -36
- package/src/providers/openai-responses-wire.ts +2 -2
- package/src/providers/openai-responses.ts +3 -4
- package/src/providers/openai-shared.ts +130 -18
- package/src/providers/xai-base-url.ts +32 -0
- package/src/registry/engine/api-key.ts +8 -3
- package/src/types.ts +35 -4
- package/src/usage/claude.ts +4 -11
- package/src/usage/cline-pass.ts +2 -14
- package/src/usage/commandcode.ts +209 -0
- package/src/usage/cursor.ts +9 -1
- package/src/usage/openai-codex.ts +3 -5
- package/src/usage/registry.ts +3 -0
- package/src/usage/shared.ts +28 -1
- package/src/usage/synthetic.ts +4 -40
- package/src/usage/umans.ts +8 -36
- package/src/usage/zai.ts +10 -38
- package/src/utils/event-stream.ts +38 -2
- package/src/utils/http-inspector.ts +4 -8
- package/src/utils/openai-http.ts +10 -3
- package/src/utils/schema/json-schema-validator.ts +23 -26
- package/src/utils/schema/meta-validator.ts +4 -7
- package/src/utils/schema/wire.ts +17 -21
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { ProviderHttpError } from "../error";
|
|
2
|
+
import type {
|
|
3
|
+
CredentialRankingStrategy,
|
|
4
|
+
UsageFetchContext,
|
|
5
|
+
UsageFetchParams,
|
|
6
|
+
UsageLimit,
|
|
7
|
+
UsageProvider,
|
|
8
|
+
UsageReport,
|
|
9
|
+
} from "../usage";
|
|
10
|
+
import { isRecord } from "../utils";
|
|
11
|
+
import { HOUR_MS, parsePositiveTimestamp, usageStatus, WEEK_MS } from "./shared";
|
|
12
|
+
|
|
13
|
+
const PROVIDER = "commandcode";
|
|
14
|
+
const DEFAULT_ORIGIN = "https://api.commandcode.ai";
|
|
15
|
+
// The CLI also sends `?limits=1` for its org spend-limit panel; omp reads no
|
|
16
|
+
// field from that, so the plain identity route is enough.
|
|
17
|
+
const WHOAMI_PATH = "/alpha/whoami";
|
|
18
|
+
const CREDITS_PATH = "/alpha/billing/credits";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Command Code's inference base carries `/provider` or `/provider/v1`, while the
|
|
22
|
+
* account routes sit at the host root, so only the configured origin is kept.
|
|
23
|
+
* A blank or unparseable override means "not configured".
|
|
24
|
+
*/
|
|
25
|
+
function resolveOrigin(baseUrl: string | undefined): string {
|
|
26
|
+
const trimmed = baseUrl?.trim();
|
|
27
|
+
if (!trimmed) return DEFAULT_ORIGIN;
|
|
28
|
+
try {
|
|
29
|
+
return new URL(trimmed).origin;
|
|
30
|
+
} catch {
|
|
31
|
+
return DEFAULT_ORIGIN;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
36
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the unwrapped JSON body, or `null` for any failure that should read
|
|
45
|
+
* as "no data", 403 included. Only a 401 throws, so a revoked key purges the
|
|
46
|
+
* cached report instead of re-serving it.
|
|
47
|
+
*/
|
|
48
|
+
async function getJson(
|
|
49
|
+
url: string,
|
|
50
|
+
apiKey: string,
|
|
51
|
+
signal: AbortSignal | undefined,
|
|
52
|
+
ctx: UsageFetchContext,
|
|
53
|
+
): Promise<Record<string, unknown> | null> {
|
|
54
|
+
try {
|
|
55
|
+
const response = await ctx.fetch(url, {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${apiKey}`,
|
|
58
|
+
Accept: "application/json",
|
|
59
|
+
},
|
|
60
|
+
signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
if (response.status === 401) {
|
|
64
|
+
throw new ProviderHttpError(
|
|
65
|
+
`Command Code usage endpoint returned ${response.status} ${response.statusText}`.trim(),
|
|
66
|
+
response.status,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
ctx.logger?.warn("Command Code usage fetch failed", {
|
|
70
|
+
url,
|
|
71
|
+
status: response.status,
|
|
72
|
+
statusText: response.statusText,
|
|
73
|
+
});
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const json: unknown = await response.json();
|
|
77
|
+
if (!isRecord(json)) return null;
|
|
78
|
+
return isRecord(json.data) ? json.data : json;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof ProviderHttpError) throw error;
|
|
81
|
+
ctx.logger?.warn("Command Code usage fetch error", { url, error: String(error) });
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface WindowSpec {
|
|
87
|
+
key: "fiveHour" | "weekly";
|
|
88
|
+
id: "5h" | "7d";
|
|
89
|
+
limitLabel: string;
|
|
90
|
+
windowLabel: string;
|
|
91
|
+
durationMs: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const WINDOWS: readonly WindowSpec[] = [
|
|
95
|
+
{ key: "fiveHour", id: "5h", limitLabel: "5-hour limit", windowLabel: "5-hour", durationMs: 5 * HOUR_MS },
|
|
96
|
+
{ key: "weekly", id: "7d", limitLabel: "Weekly limit", windowLabel: "Weekly", durationMs: WEEK_MS },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
function buildWindowLimit(
|
|
100
|
+
spec: WindowSpec,
|
|
101
|
+
raw: unknown,
|
|
102
|
+
accountId: string,
|
|
103
|
+
orgId: string | undefined,
|
|
104
|
+
): UsageLimit | undefined {
|
|
105
|
+
if (!isRecord(raw)) return undefined;
|
|
106
|
+
const used = finiteNumber(raw.used);
|
|
107
|
+
if (used === undefined) return undefined;
|
|
108
|
+
const cap = finiteNumber(raw.cap);
|
|
109
|
+
const usedFraction = cap !== undefined && cap > 0 ? used / cap : undefined;
|
|
110
|
+
const resetsAt = parsePositiveTimestamp(raw.resetAt);
|
|
111
|
+
// Absent fields are omitted, not set to `undefined`: the usage wire schema
|
|
112
|
+
// rejects an explicit `undefined` for an optional key.
|
|
113
|
+
return {
|
|
114
|
+
id: `${PROVIDER}:${spec.id}`,
|
|
115
|
+
label: spec.limitLabel,
|
|
116
|
+
scope: { provider: PROVIDER, accountId, ...(orgId ? { orgId } : {}), windowId: spec.id, shared: true },
|
|
117
|
+
window: {
|
|
118
|
+
id: spec.id,
|
|
119
|
+
label: spec.windowLabel,
|
|
120
|
+
durationMs: spec.durationMs,
|
|
121
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
122
|
+
},
|
|
123
|
+
amount: {
|
|
124
|
+
used,
|
|
125
|
+
...(cap !== undefined ? { limit: cap } : {}),
|
|
126
|
+
...(usedFraction !== undefined ? { usedFraction } : {}),
|
|
127
|
+
unit: "credits",
|
|
128
|
+
},
|
|
129
|
+
status: raw.exceeded === true ? "exhausted" : usageStatus(usedFraction),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reads the account routes that the Command Code CLI calls with the same API
|
|
135
|
+
* key; the Provider API documents no usage endpoint. Pay-as-you-go accounts
|
|
136
|
+
* carry no `windowLimits`, so they report only the credit balance. Credits
|
|
137
|
+
* are queried per org, as the CLI does, so each limit records the org that
|
|
138
|
+
* owns the pool alongside the user.
|
|
139
|
+
*/
|
|
140
|
+
async function fetchCommandCodeUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
|
141
|
+
if (params.provider !== PROVIDER) return null;
|
|
142
|
+
if (params.credential.type !== "api_key" || !params.credential.apiKey) return null;
|
|
143
|
+
const origin = resolveOrigin(params.baseUrl);
|
|
144
|
+
|
|
145
|
+
const whoami = await getJson(`${origin}${WHOAMI_PATH}`, params.credential.apiKey, params.signal, ctx);
|
|
146
|
+
if (!whoami) return null;
|
|
147
|
+
const user = isRecord(whoami.user) ? whoami.user : undefined;
|
|
148
|
+
const org = isRecord(whoami.org) ? whoami.org : undefined;
|
|
149
|
+
const userId = nonEmptyString(user?.id);
|
|
150
|
+
if (!userId) return null;
|
|
151
|
+
const orgId = nonEmptyString(org?.id);
|
|
152
|
+
const orgLogin = nonEmptyString(org?.login);
|
|
153
|
+
|
|
154
|
+
const creditsUrl = `${origin}${CREDITS_PATH}${orgId ? `?orgId=${encodeURIComponent(orgId)}` : ""}`;
|
|
155
|
+
const creditsBody = await getJson(creditsUrl, params.credential.apiKey, params.signal, ctx);
|
|
156
|
+
if (!creditsBody) return null;
|
|
157
|
+
const credits = isRecord(creditsBody.credits) ? creditsBody.credits : undefined;
|
|
158
|
+
if (!credits) return null;
|
|
159
|
+
const monthly = finiteNumber(credits.monthlyCredits);
|
|
160
|
+
const purchased = finiteNumber(credits.purchasedCredits);
|
|
161
|
+
const free = finiteNumber(credits.freeCredits);
|
|
162
|
+
if (monthly === undefined && purchased === undefined && free === undefined) return null;
|
|
163
|
+
|
|
164
|
+
const limits: UsageLimit[] = [];
|
|
165
|
+
const windowLimits = isRecord(creditsBody.windowLimits) ? creditsBody.windowLimits : undefined;
|
|
166
|
+
for (const spec of WINDOWS) {
|
|
167
|
+
const limit = buildWindowLimit(spec, windowLimits?.[spec.key], userId, orgId);
|
|
168
|
+
if (limit) limits.push(limit);
|
|
169
|
+
}
|
|
170
|
+
limits.push({
|
|
171
|
+
id: `${PROVIDER}:balance`,
|
|
172
|
+
label: "Credit balance",
|
|
173
|
+
scope: { provider: PROVIDER, accountId: userId, ...(orgId ? { orgId } : {}), windowId: "balance", shared: true },
|
|
174
|
+
amount: { remaining: (monthly ?? 0) + (purchased ?? 0) + (free ?? 0), unit: "credits" },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
provider: PROVIDER,
|
|
179
|
+
fetchedAt: Date.now(),
|
|
180
|
+
limits,
|
|
181
|
+
metadata: {
|
|
182
|
+
accountId: userId,
|
|
183
|
+
email: nonEmptyString(user?.email),
|
|
184
|
+
orgId,
|
|
185
|
+
orgName: orgLogin,
|
|
186
|
+
planType: nonEmptyString(credits.planId),
|
|
187
|
+
},
|
|
188
|
+
raw: { whoami, credits: creditsBody },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const commandCodeUsageProvider: UsageProvider = {
|
|
193
|
+
id: PROVIDER,
|
|
194
|
+
fetchUsage: fetchCommandCodeUsage,
|
|
195
|
+
supports: params => params.provider === PROVIDER && params.credential.type === "api_key",
|
|
196
|
+
validatesCredentials: true,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** Ranks Command Code accounts by the 5-hour and weekly credit windows. */
|
|
200
|
+
export const commandCodeRankingStrategy: CredentialRankingStrategy = {
|
|
201
|
+
findWindowLimits: report => ({
|
|
202
|
+
primary: report.limits.find(limit => limit.window?.id === "5h"),
|
|
203
|
+
secondary: report.limits.find(limit => limit.window?.id === "7d"),
|
|
204
|
+
}),
|
|
205
|
+
windowDefaults: {
|
|
206
|
+
primaryMs: 5 * HOUR_MS,
|
|
207
|
+
secondaryMs: WEEK_MS,
|
|
208
|
+
},
|
|
209
|
+
};
|
package/src/usage/cursor.ts
CHANGED
|
@@ -478,10 +478,18 @@ export const cursorUsageProvider: UsageProvider = {
|
|
|
478
478
|
]);
|
|
479
479
|
let report: UsageReport | null;
|
|
480
480
|
if (legacyReport && summaryReport) {
|
|
481
|
+
// `/auth/usage` is Cursor's request-count API from before usage-based
|
|
482
|
+
// plans. Current plans answer it with an uncapped, always-zero `gpt-4`
|
|
483
|
+
// bucket; beside the summary's dollar rails an uncapped, unused bucket
|
|
484
|
+
// carries nothing, so it is dropped. Capped buckets and uncapped buckets
|
|
485
|
+
// with recorded requests stay. Without a summary it stays the fallback.
|
|
486
|
+
const legacyLimits = legacyReport.limits.filter(
|
|
487
|
+
limit => limit.amount.limit !== undefined || (limit.amount.used ?? 0) > 0,
|
|
488
|
+
);
|
|
481
489
|
report = {
|
|
482
490
|
provider: "cursor",
|
|
483
491
|
fetchedAt,
|
|
484
|
-
limits: [...
|
|
492
|
+
limits: [...legacyLimits, ...summaryReport.limits],
|
|
485
493
|
raw: {
|
|
486
494
|
authUsage: legacyReport.raw,
|
|
487
495
|
usageSummary: summaryReport.raw,
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
import { isRecord } from "../utils";
|
|
19
19
|
import { normalizeCodexBaseUrl } from "./openai-codex-base-url";
|
|
20
20
|
import { listCodexResetCredits } from "./openai-codex-reset";
|
|
21
|
-
import { HOUR_MS } from "./shared";
|
|
21
|
+
import { HOUR_MS, usageStatus } from "./shared";
|
|
22
22
|
|
|
23
23
|
const CODEX_USAGE_PATH = "wham/usage";
|
|
24
24
|
const CODEX_VERIFIED_ACCESS_PATH = "accounts/verified_access";
|
|
@@ -341,10 +341,8 @@ function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
function buildUsageStatus(args: { usedFraction?: number; explicitlyAllowed: boolean }): UsageLimit["status"] {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (args.usedFraction >= 0.9) return "warning";
|
|
347
|
-
return "ok";
|
|
344
|
+
const status = usageStatus(args.usedFraction);
|
|
345
|
+
return status === "exhausted" && args.explicitlyAllowed ? "warning" : status;
|
|
348
346
|
}
|
|
349
347
|
|
|
350
348
|
/**
|
package/src/usage/registry.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { alibabaTokenPlanRankingStrategy, alibabaTokenPlanUsageProvider } from "
|
|
|
4
4
|
import { charmHyperUsageProvider } from "./charm-hyper";
|
|
5
5
|
import { claudeRankingStrategy, claudeUsageProvider } from "./claude";
|
|
6
6
|
import { clinePassUsageProvider } from "./cline-pass";
|
|
7
|
+
import { commandCodeRankingStrategy, commandCodeUsageProvider } from "./commandcode";
|
|
7
8
|
import { cursorRankingStrategy, cursorUsageProvider } from "./cursor";
|
|
8
9
|
import { devinUsageProvider } from "./devin";
|
|
9
10
|
import { googleGeminiCliUsageProvider } from "./gemini";
|
|
@@ -45,6 +46,7 @@ export const DEFAULT_USAGE_PROVIDERS: readonly UsageProvider[] = [
|
|
|
45
46
|
xaiOauthUsageProvider,
|
|
46
47
|
devinUsageProvider,
|
|
47
48
|
charmHyperUsageProvider,
|
|
49
|
+
commandCodeUsageProvider,
|
|
48
50
|
];
|
|
49
51
|
|
|
50
52
|
const DEFAULT_USAGE_PROVIDER_MAP = new Map<Provider, UsageProvider>(
|
|
@@ -66,6 +68,7 @@ const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>(
|
|
|
66
68
|
["zai", zaiRankingStrategy],
|
|
67
69
|
["opencode-go", opencodeGoRankingStrategy],
|
|
68
70
|
["xai-oauth", xaiOauthRankingStrategy],
|
|
71
|
+
["commandcode", commandCodeRankingStrategy],
|
|
69
72
|
]);
|
|
70
73
|
|
|
71
74
|
/** Built-in ranking strategy for `provider`. */
|
package/src/usage/shared.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
2
|
-
import type { UsageStatus } from "../usage";
|
|
2
|
+
import type { UsageAmount, UsageStatus } from "../usage";
|
|
3
3
|
|
|
4
4
|
/** Milliseconds in one hour. */
|
|
5
5
|
export const HOUR_MS = 60 * 60 * 1000;
|
|
@@ -31,3 +31,30 @@ export function usageStatus(usedFraction: number | undefined): UsageStatus {
|
|
|
31
31
|
if (usedFraction >= 0.9) return "warning";
|
|
32
32
|
return "ok";
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds an amount from absolute counters. Without an authoritative
|
|
37
|
+
* `usedFraction`, it derives one from `used / limit` (capped at 1).
|
|
38
|
+
* Undefined fields are omitted rather than emitted as `undefined` keys.
|
|
39
|
+
*/
|
|
40
|
+
export function buildUsageAmount(args: {
|
|
41
|
+
used: number | undefined;
|
|
42
|
+
limit: number | undefined;
|
|
43
|
+
remaining: number | undefined;
|
|
44
|
+
usedFraction?: number;
|
|
45
|
+
unit: UsageAmount["unit"];
|
|
46
|
+
}): UsageAmount {
|
|
47
|
+
let usedFraction = args.usedFraction;
|
|
48
|
+
if (usedFraction === undefined && args.used !== undefined && args.limit !== undefined && args.limit > 0) {
|
|
49
|
+
usedFraction = Math.min(args.used / args.limit, 1);
|
|
50
|
+
}
|
|
51
|
+
const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
|
|
52
|
+
return {
|
|
53
|
+
...(args.used !== undefined ? { used: args.used } : {}),
|
|
54
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
55
|
+
...(args.remaining !== undefined ? { remaining: args.remaining } : {}),
|
|
56
|
+
...(usedFraction !== undefined ? { usedFraction } : {}),
|
|
57
|
+
...(remainingFraction !== undefined ? { remainingFraction } : {}),
|
|
58
|
+
unit: args.unit,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/usage/synthetic.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "@oh-my-pi/pi-utils/type-guards";
|
|
2
2
|
import type {
|
|
3
|
-
UsageAmount,
|
|
4
3
|
UsageFetchContext,
|
|
5
4
|
UsageFetchParams,
|
|
6
5
|
UsageLimit,
|
|
@@ -9,7 +8,7 @@ import type {
|
|
|
9
8
|
UsageStatus,
|
|
10
9
|
UsageWindow,
|
|
11
10
|
} from "../usage";
|
|
12
|
-
import { HOUR_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
|
|
11
|
+
import { buildUsageAmount, HOUR_MS, parseIsoTimestamp, usageStatus, WEEK_MS } from "./shared";
|
|
13
12
|
|
|
14
13
|
const QUOTAS_URL = "https://api.synthetic.new/v2/quotas";
|
|
15
14
|
|
|
@@ -20,35 +19,6 @@ function parseDollarAmount(value: unknown): number | undefined {
|
|
|
20
19
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
function buildUsageAmount(args: {
|
|
24
|
-
used: number | undefined;
|
|
25
|
-
limit: number | undefined;
|
|
26
|
-
remaining: number | undefined;
|
|
27
|
-
usedFraction: number | undefined;
|
|
28
|
-
unit: UsageAmount["unit"];
|
|
29
|
-
}): UsageAmount {
|
|
30
|
-
let usedFraction = args.usedFraction;
|
|
31
|
-
if (usedFraction === undefined && args.used !== undefined && args.limit !== undefined && args.limit > 0) {
|
|
32
|
-
usedFraction = Math.min(args.used / args.limit, 1);
|
|
33
|
-
}
|
|
34
|
-
const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
|
|
35
|
-
return {
|
|
36
|
-
...(args.used !== undefined ? { used: args.used } : {}),
|
|
37
|
-
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
38
|
-
...(args.remaining !== undefined ? { remaining: args.remaining } : {}),
|
|
39
|
-
...(usedFraction !== undefined ? { usedFraction } : {}),
|
|
40
|
-
...(remainingFraction !== undefined ? { remainingFraction } : {}),
|
|
41
|
-
unit: args.unit,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
|
|
46
|
-
if (usedFraction === undefined) return undefined;
|
|
47
|
-
if (usedFraction >= 1) return "exhausted";
|
|
48
|
-
if (usedFraction >= 0.9) return "warning";
|
|
49
|
-
return "ok";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
22
|
function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["provider"]): UsageLimit | null {
|
|
53
23
|
if (!isRecord(raw)) return null;
|
|
54
24
|
const remaining = typeof raw.remaining === "number" ? raw.remaining : undefined;
|
|
@@ -67,14 +37,8 @@ function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["pro
|
|
|
67
37
|
durationMs: 5 * HOUR_MS,
|
|
68
38
|
...(nextTickAt !== undefined ? { resetsAt: nextTickAt, resetLabel: "tick" } : {}),
|
|
69
39
|
};
|
|
70
|
-
const amount = buildUsageAmount({
|
|
71
|
-
|
|
72
|
-
limit: max,
|
|
73
|
-
remaining,
|
|
74
|
-
usedFraction: undefined,
|
|
75
|
-
unit: "requests",
|
|
76
|
-
});
|
|
77
|
-
const status: UsageStatus = limited ? "exhausted" : (getUsageStatus(amount.usedFraction) ?? "ok");
|
|
40
|
+
const amount = buildUsageAmount({ used, limit: max, remaining, unit: "requests" });
|
|
41
|
+
const status: UsageStatus = limited ? "exhausted" : usageStatus(amount.usedFraction ?? 0);
|
|
78
42
|
return {
|
|
79
43
|
id: "synthetic:requests:5h",
|
|
80
44
|
label: "Synthetic Requests",
|
|
@@ -117,7 +81,7 @@ function parseWeeklyTokenLimit(raw: unknown, provider: UsageFetchParams["provide
|
|
|
117
81
|
scope: { provider, windowId: "7d", shared: true },
|
|
118
82
|
window,
|
|
119
83
|
amount,
|
|
120
|
-
status:
|
|
84
|
+
status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
|
|
121
85
|
};
|
|
122
86
|
}
|
|
123
87
|
|
package/src/usage/umans.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ProviderHttpError } from "../error";
|
|
2
2
|
import type {
|
|
3
|
-
UsageAmount,
|
|
4
3
|
UsageFetchContext,
|
|
5
4
|
UsageFetchParams,
|
|
6
5
|
UsageLimit,
|
|
@@ -10,7 +9,7 @@ import type {
|
|
|
10
9
|
UsageWindow,
|
|
11
10
|
} from "../usage";
|
|
12
11
|
import { isRecord } from "../utils";
|
|
13
|
-
import { HOUR_MS } from "./shared";
|
|
12
|
+
import { buildUsageAmount, HOUR_MS, usageStatus } from "./shared";
|
|
14
13
|
|
|
15
14
|
const UMANS_PROVIDER = "umans";
|
|
16
15
|
const DEFAULT_ENDPOINT = "https://api.code.umans.ai";
|
|
@@ -53,13 +52,6 @@ function toFiniteNumber(value: unknown): number | undefined {
|
|
|
53
52
|
return value;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
function resolveStatus(usedFraction: number | undefined): UsageStatus | undefined {
|
|
57
|
-
if (usedFraction === undefined) return undefined;
|
|
58
|
-
if (usedFraction >= 1) return "exhausted";
|
|
59
|
-
if (usedFraction >= 0.9) return "warning";
|
|
60
|
-
return "ok";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
55
|
/**
|
|
64
56
|
* Soft-cap status never reaches `exhausted`: hitting the effective-request
|
|
65
57
|
* limit only means burst headroom is being consumed — Umans throttles (429)
|
|
@@ -72,26 +64,6 @@ function softCapStatus(usedFraction: number | undefined): UsageStatus | undefine
|
|
|
72
64
|
return "ok";
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
function buildAmount(args: {
|
|
76
|
-
used: number | undefined;
|
|
77
|
-
limit: number | undefined;
|
|
78
|
-
remaining: number | undefined;
|
|
79
|
-
unit: UsageAmount["unit"];
|
|
80
|
-
}): UsageAmount {
|
|
81
|
-
const used = args.used;
|
|
82
|
-
const limit = args.limit;
|
|
83
|
-
const usedFraction = used !== undefined && limit !== undefined && limit > 0 ? Math.min(used / limit, 1) : undefined;
|
|
84
|
-
const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
|
|
85
|
-
return {
|
|
86
|
-
used,
|
|
87
|
-
limit,
|
|
88
|
-
remaining: args.remaining,
|
|
89
|
-
usedFraction,
|
|
90
|
-
remainingFraction,
|
|
91
|
-
unit: args.unit,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
67
|
function buildRequestsLimits(payload: UmansUsagePayload, provider: string): UsageLimit[] {
|
|
96
68
|
const limit = toFiniteNumber(payload.limits?.requests?.limit);
|
|
97
69
|
const hardCap = toFiniteNumber(payload.limits?.requests?.hard_cap);
|
|
@@ -127,7 +99,7 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
|
|
|
127
99
|
// limit still never drives exhaustion on its own: weighted headroom stays
|
|
128
100
|
// decisive (https://github.com/can1357/oh-my-pi/issues/7858).
|
|
129
101
|
if (weightedUsed === undefined || hardCap === undefined) {
|
|
130
|
-
const amount =
|
|
102
|
+
const amount = buildUsageAmount({
|
|
131
103
|
used: weightedUsed ?? rawUsed,
|
|
132
104
|
limit,
|
|
133
105
|
remaining: weightedUsed !== undefined ? weightedRemaining : rawRemaining,
|
|
@@ -140,7 +112,7 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
|
|
|
140
112
|
scope: { provider, windowId: window.id, shared: true },
|
|
141
113
|
window,
|
|
142
114
|
amount,
|
|
143
|
-
status:
|
|
115
|
+
status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
|
|
144
116
|
},
|
|
145
117
|
];
|
|
146
118
|
}
|
|
@@ -151,7 +123,7 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
|
|
|
151
123
|
// read as exhausted mid-window while the account still has weighted
|
|
152
124
|
// headroom (https://github.com/can1357/oh-my-pi/issues/7858). Soft cap hits
|
|
153
125
|
// warn; only the burst ceiling (`hard_cap`, raw counts) can exhaust.
|
|
154
|
-
const softAmount =
|
|
126
|
+
const softAmount = buildUsageAmount({ used: weightedUsed, limit, remaining: weightedRemaining, unit: "requests" });
|
|
155
127
|
const limits: UsageLimit[] = [
|
|
156
128
|
{
|
|
157
129
|
id: "umans:requests:soft",
|
|
@@ -163,14 +135,14 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
|
|
|
163
135
|
},
|
|
164
136
|
];
|
|
165
137
|
if (hardCap !== undefined && rawUsed !== undefined) {
|
|
166
|
-
const hardAmount =
|
|
138
|
+
const hardAmount = buildUsageAmount({ used: rawUsed, limit: hardCap, remaining: undefined, unit: "requests" });
|
|
167
139
|
limits.push({
|
|
168
140
|
id: "umans:requests:hard",
|
|
169
141
|
label: "Requests (burst ceiling)",
|
|
170
142
|
scope: { provider, windowId: window.id, shared: true },
|
|
171
143
|
window,
|
|
172
144
|
amount: hardAmount,
|
|
173
|
-
status:
|
|
145
|
+
status: hardAmount.usedFraction === undefined ? undefined : usageStatus(hardAmount.usedFraction),
|
|
174
146
|
});
|
|
175
147
|
}
|
|
176
148
|
return limits;
|
|
@@ -180,14 +152,14 @@ function buildConcurrencyLimit(payload: UmansUsagePayload, provider: string): Us
|
|
|
180
152
|
const limit = toFiniteNumber(payload.limits?.concurrency?.limit);
|
|
181
153
|
const used = toFiniteNumber(payload.usage?.concurrent_sessions);
|
|
182
154
|
if (limit === undefined && used === undefined) return null;
|
|
183
|
-
const amount =
|
|
155
|
+
const amount = buildUsageAmount({ used, limit, remaining: undefined, unit: "requests" });
|
|
184
156
|
return {
|
|
185
157
|
id: "umans:concurrency",
|
|
186
158
|
label: "Concurrency",
|
|
187
159
|
// Concurrency is instantaneous, not windowed.
|
|
188
160
|
scope: { provider, windowId: "concurrency" },
|
|
189
161
|
amount,
|
|
190
|
-
status:
|
|
162
|
+
status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
|
|
191
163
|
};
|
|
192
164
|
}
|
|
193
165
|
|
package/src/usage/zai.ts
CHANGED
|
@@ -2,17 +2,15 @@ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
|
2
2
|
import { USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type {
|
|
4
4
|
CredentialRankingStrategy,
|
|
5
|
-
UsageAmount,
|
|
6
5
|
UsageFetchContext,
|
|
7
6
|
UsageFetchParams,
|
|
8
7
|
UsageLimit,
|
|
9
8
|
UsageProvider,
|
|
10
9
|
UsageReport,
|
|
11
|
-
UsageStatus,
|
|
12
10
|
UsageWindow,
|
|
13
11
|
} from "../usage";
|
|
14
12
|
import { isRecord } from "../utils";
|
|
15
|
-
import { DAY_MS, HOUR_MS, WEEK_MS } from "./shared";
|
|
13
|
+
import { buildUsageAmount, DAY_MS, HOUR_MS, usageStatus, WEEK_MS } from "./shared";
|
|
16
14
|
|
|
17
15
|
const DEFAULT_ENDPOINT = "https://api.z.ai";
|
|
18
16
|
const QUOTA_PATH = "/api/monitor/usage/quota/limit";
|
|
@@ -94,35 +92,9 @@ function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
|
|
|
94
92
|
};
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
remaining: number | undefined;
|
|
101
|
-
unit: UsageAmount["unit"];
|
|
102
|
-
percentage?: number;
|
|
103
|
-
}): UsageAmount {
|
|
104
|
-
const usedFraction =
|
|
105
|
-
args.percentage !== undefined
|
|
106
|
-
? Math.min(Math.max(args.percentage / 100, 0), 1)
|
|
107
|
-
: args.used !== undefined && args.limit !== undefined && args.limit > 0
|
|
108
|
-
? Math.min(args.used / args.limit, 1)
|
|
109
|
-
: undefined;
|
|
110
|
-
const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
|
|
111
|
-
return {
|
|
112
|
-
used: args.used,
|
|
113
|
-
limit: args.limit,
|
|
114
|
-
remaining: args.remaining,
|
|
115
|
-
usedFraction,
|
|
116
|
-
remainingFraction,
|
|
117
|
-
unit: args.unit,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
|
|
122
|
-
if (usedFraction === undefined) return undefined;
|
|
123
|
-
if (usedFraction >= 1) return "exhausted";
|
|
124
|
-
if (usedFraction >= 0.9) return "warning";
|
|
125
|
-
return "ok";
|
|
95
|
+
/** Z.AI `percentage` is 0-100 and may overshoot; clamp it to a used fraction. */
|
|
96
|
+
function percentageFraction(percentage: number | undefined): number | undefined {
|
|
97
|
+
return percentage === undefined ? undefined : Math.min(Math.max(percentage / 100, 0), 1);
|
|
126
98
|
}
|
|
127
99
|
|
|
128
100
|
function formatDate(value: Date): string {
|
|
@@ -283,7 +255,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
283
255
|
used: parsed.currentValue,
|
|
284
256
|
limit: parsed.usage,
|
|
285
257
|
remaining: parsed.remaining,
|
|
286
|
-
|
|
258
|
+
usedFraction: percentageFraction(parsed.percentage),
|
|
287
259
|
unit: "tokens",
|
|
288
260
|
});
|
|
289
261
|
const window = buildZaiWindow(parsed);
|
|
@@ -297,7 +269,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
297
269
|
},
|
|
298
270
|
window,
|
|
299
271
|
amount,
|
|
300
|
-
status:
|
|
272
|
+
status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
|
|
301
273
|
});
|
|
302
274
|
}
|
|
303
275
|
if (parsed.type === "TIME_LIMIT") {
|
|
@@ -306,7 +278,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
306
278
|
used: parsed.currentValue,
|
|
307
279
|
limit: parsed.usage,
|
|
308
280
|
remaining: parsed.remaining,
|
|
309
|
-
|
|
281
|
+
usedFraction: percentageFraction(parsed.percentage),
|
|
310
282
|
unit: "requests",
|
|
311
283
|
});
|
|
312
284
|
const featureLimit = isZaiFeatureRequestLimit(parsed);
|
|
@@ -321,7 +293,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
321
293
|
},
|
|
322
294
|
window,
|
|
323
295
|
amount,
|
|
324
|
-
status:
|
|
296
|
+
status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
|
|
325
297
|
});
|
|
326
298
|
}
|
|
327
299
|
if (parsed.type === "CREDIT_LIMIT") {
|
|
@@ -335,7 +307,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
335
307
|
used: parsed.currentValue,
|
|
336
308
|
limit: parsed.usage,
|
|
337
309
|
remaining: parsed.remaining,
|
|
338
|
-
|
|
310
|
+
usedFraction: hasAbsoluteMeter ? undefined : percentageFraction(parsed.percentage),
|
|
339
311
|
unit: "credits",
|
|
340
312
|
});
|
|
341
313
|
limits.push({
|
|
@@ -348,7 +320,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
348
320
|
},
|
|
349
321
|
window,
|
|
350
322
|
amount,
|
|
351
|
-
status:
|
|
323
|
+
status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
|
|
352
324
|
});
|
|
353
325
|
}
|
|
354
326
|
}
|
|
@@ -6,9 +6,21 @@ export interface LocalWorkSource {
|
|
|
6
6
|
readonly hasPendingLocalWork: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/** Consumed head slots tolerated before the backlog is compacted (see {@link EventStream.queue}). */
|
|
10
|
+
const QUEUE_COMPACT_MIN_HEAD = 64;
|
|
11
|
+
|
|
9
12
|
// Generic event stream class for async iteration
|
|
10
13
|
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
14
|
+
/**
|
|
15
|
+
* Events pushed while no consumer was waiting. The iterator dequeues by
|
|
16
|
+
* advancing {@link #queueHead} instead of `shift()` — O(remaining) per event,
|
|
17
|
+
* quadratic for a consumer draining a backlog — so while it drains, the
|
|
18
|
+
* slots before the head are consumed (cleared) placeholders. Do not mutate
|
|
19
|
+
* the array while the stream is being iterated.
|
|
20
|
+
*/
|
|
11
21
|
queue: T[] = [];
|
|
22
|
+
/** Index of the next undelivered event in {@link queue}; 0 whenever the queue is empty. */
|
|
23
|
+
#queueHead = 0;
|
|
12
24
|
waiting: Array<{ resolve: (value: IteratorResult<T>) => void; reject: (err: unknown) => void }> = [];
|
|
13
25
|
done = false;
|
|
14
26
|
/** True once finalResultPromise has been resolved or rejected. */
|
|
@@ -115,10 +127,34 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Take the event at the queue head. Clears the consumed slot so it stops
|
|
132
|
+
* retaining the event, resets the queue once drained, and compacts it once
|
|
133
|
+
* consumed slots are at least half of it (amortized O(1) per event).
|
|
134
|
+
*/
|
|
135
|
+
#dequeue(): T {
|
|
136
|
+
const queue = this.queue;
|
|
137
|
+
const head = this.#queueHead;
|
|
138
|
+
const event = queue[head];
|
|
139
|
+
if (head + 1 === queue.length) {
|
|
140
|
+
queue.length = 0;
|
|
141
|
+
this.#queueHead = 0;
|
|
142
|
+
return event;
|
|
143
|
+
}
|
|
144
|
+
// The slot is dead once the head moves past it; `undefined` only drops the reference.
|
|
145
|
+
queue[head] = undefined as T;
|
|
146
|
+
this.#queueHead = head + 1;
|
|
147
|
+
if (this.#queueHead >= QUEUE_COMPACT_MIN_HEAD && this.#queueHead * 2 >= queue.length) {
|
|
148
|
+
queue.splice(0, this.#queueHead);
|
|
149
|
+
this.#queueHead = 0;
|
|
150
|
+
}
|
|
151
|
+
return event;
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
119
155
|
while (true) {
|
|
120
|
-
if (this.queue.length
|
|
121
|
-
yield this
|
|
156
|
+
if (this.#queueHead < this.queue.length) {
|
|
157
|
+
yield this.#dequeue();
|
|
122
158
|
} else if (this.#failed) {
|
|
123
159
|
throw this.#error;
|
|
124
160
|
} else if (this.done) {
|