@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/types/auth-retry.d.ts +3 -3
  3. package/dist/types/auth-storage.d.ts +2 -0
  4. package/dist/types/error/auth-classify.d.ts +5 -5
  5. package/dist/types/error/aws.d.ts +5 -1
  6. package/dist/types/error/flags.d.ts +4 -0
  7. package/dist/types/providers/aws-credentials.d.ts +4 -3
  8. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  9. package/dist/types/providers/cursor-pi-args.d.ts +14 -0
  10. package/dist/types/providers/openai-shared.d.ts +9 -1
  11. package/dist/types/registry/api-key-login.d.ts +6 -4
  12. package/dist/types/registry/cloudflare-ai-gateway.d.ts +2 -2
  13. package/dist/types/registry/kagi.d.ts +2 -2
  14. package/dist/types/registry/litellm.d.ts +2 -2
  15. package/dist/types/registry/llama-cpp.d.ts +2 -2
  16. package/dist/types/registry/lm-studio.d.ts +2 -2
  17. package/dist/types/registry/parallel.d.ts +2 -2
  18. package/dist/types/registry/tavily.d.ts +1 -1
  19. package/dist/types/registry/vercel-ai-gateway.d.ts +2 -2
  20. package/dist/types/registry/vllm.d.ts +2 -2
  21. package/dist/types/types.d.ts +7 -0
  22. package/dist/types/usage/cursor.d.ts +11 -0
  23. package/dist/types/usage/openai-codex-reset.d.ts +0 -20
  24. package/dist/types/usage/shared.d.ts +13 -1
  25. package/dist/types/utils/block-symbols.d.ts +12 -0
  26. package/package.json +5 -5
  27. package/src/auth-retry.ts +5 -4
  28. package/src/auth-storage.ts +77 -33
  29. package/src/dialect/owned-stream.ts +3 -0
  30. package/src/error/auth-classify.ts +7 -6
  31. package/src/error/aws.ts +5 -1
  32. package/src/error/flags.ts +14 -0
  33. package/src/providers/amazon-bedrock.ts +38 -0
  34. package/src/providers/aws-credentials.ts +222 -29
  35. package/src/providers/cursor/exec-modern.ts +1 -0
  36. package/src/providers/cursor-pi-args.ts +22 -0
  37. package/src/providers/cursor.ts +81 -1
  38. package/src/providers/google-gemini-cli.ts +49 -15
  39. package/src/providers/google-shared.ts +7 -1
  40. package/src/providers/openai-codex/request-transformer.ts +38 -17
  41. package/src/providers/openai-codex-responses.ts +2 -3
  42. package/src/providers/openai-responses.ts +4 -0
  43. package/src/providers/openai-shared.ts +55 -1
  44. package/src/providers/pi-native-server.ts +1 -0
  45. package/src/providers/register-builtins.ts +18 -14
  46. package/src/registry/api-key-login.ts +26 -12
  47. package/src/registry/aws.ts +13 -6
  48. package/src/registry/cloudflare-ai-gateway.ts +10 -29
  49. package/src/registry/kagi.ts +11 -29
  50. package/src/registry/litellm.ts +11 -29
  51. package/src/registry/llama-cpp.ts +11 -21
  52. package/src/registry/lm-studio.ts +9 -20
  53. package/src/registry/oauth/callback-server.ts +93 -5
  54. package/src/registry/parallel.ts +10 -28
  55. package/src/registry/tavily.ts +9 -27
  56. package/src/registry/vercel-ai-gateway.ts +10 -28
  57. package/src/registry/vllm.ts +11 -21
  58. package/src/stream.ts +10 -7
  59. package/src/types.ts +7 -0
  60. package/src/usage/alibaba-token-plan.ts +8 -15
  61. package/src/usage/claude.ts +12 -19
  62. package/src/usage/cursor.ts +179 -59
  63. package/src/usage/gemini.ts +3 -2
  64. package/src/usage/github-copilot.ts +3 -2
  65. package/src/usage/google-antigravity.ts +10 -17
  66. package/src/usage/kimi.ts +35 -17
  67. package/src/usage/minimax-code.ts +7 -27
  68. package/src/usage/openai-codex-reset.ts +3 -2
  69. package/src/usage/openai-codex.ts +5 -5
  70. package/src/usage/opencode-go.ts +1 -2
  71. package/src/usage/shared.ts +33 -10
  72. package/src/usage/synthetic.ts +4 -11
  73. package/src/usage/umans.ts +2 -2
  74. package/src/usage/xai-oauth.ts +13 -26
  75. package/src/usage/zai.ts +4 -6
  76. package/src/utils/aws-profile.ts +39 -1
  77. package/src/utils/block-symbols.ts +18 -0
  78. package/src/utils/leaked-thinking-stream.ts +3 -0
  79. package/src/utils/openrouter-headers.ts +3 -3
@@ -1,18 +1,11 @@
1
- import type {
2
- UsageFetchContext,
3
- UsageFetchParams,
4
- UsageLimit,
5
- UsageProvider,
6
- UsageReport,
7
- UsageStatus,
8
- } from "../usage";
1
+ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
2
+ import type { UsageFetchContext, UsageFetchParams, UsageLimit, UsageProvider, UsageReport } from "../usage";
9
3
  import { isRecord } from "../utils";
10
- import { toNumber } from "./shared";
4
+ import { HOUR_MS, parsePositiveTimestamp, usageStatus } from "./shared";
11
5
 
12
6
  const INTL_PROVIDER = "minimax-code";
13
7
  const INTL_BASE_URL = "https://api.minimax.io";
14
8
  const REMAINS_PATH = "/v1/token_plan/remains";
15
- const HOUR_MS = 60 * 60 * 1000;
16
9
  /** `current_*_status` enum reported per window: 1 normal, 2 exhausted, 3 unlimited. */
17
10
  const STATUS_EXHAUSTED = 2;
18
11
  const STATUS_UNLIMITED = 3;
@@ -42,13 +35,6 @@ interface TokenPlanBucket {
42
35
  weeklyStatus?: number;
43
36
  }
44
37
 
45
- /** MiniMax reports epoch milliseconds; tolerate seconds in case a deployment differs. */
46
- function parseTimestamp(value: unknown): number | undefined {
47
- const parsed = toNumber(value);
48
- if (parsed === undefined || parsed <= 0) return undefined;
49
- return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
50
- }
51
-
52
38
  /** `current_*_remaining_percent` is 0..100 remaining; usage fractions are 0..1 used. */
53
39
  function usedFractionFromRemainingPercent(value: unknown): number | undefined {
54
40
  const parsed = toNumber(value);
@@ -57,26 +43,20 @@ function usedFractionFromRemainingPercent(value: unknown): number | undefined {
57
43
  return Math.min(1, Math.max(0, (100 - parsed) / 100));
58
44
  }
59
45
 
60
- function usageStatus(usedFraction: number): UsageStatus {
61
- if (usedFraction >= 1) return "exhausted";
62
- if (usedFraction >= 0.9) return "warning";
63
- return "ok";
64
- }
65
-
66
46
  function parseBucket(value: unknown): TokenPlanBucket | null {
67
47
  if (!isRecord(value)) return null;
68
48
  const modelName = typeof value.model_name === "string" ? value.model_name.trim() : "";
69
49
  if (!modelName) return null;
70
50
  return {
71
51
  modelName,
72
- intervalStart: parseTimestamp(value.start_time),
73
- intervalEnd: parseTimestamp(value.end_time),
52
+ intervalStart: parsePositiveTimestamp(value.start_time),
53
+ intervalEnd: parsePositiveTimestamp(value.end_time),
74
54
  intervalRemainingPercent: toNumber(value.current_interval_remaining_percent),
75
55
  intervalTotalCount: toNumber(value.current_interval_total_count),
76
56
  intervalUsageCount: toNumber(value.current_interval_usage_count),
77
57
  intervalStatus: toNumber(value.current_interval_status),
78
- weeklyStart: parseTimestamp(value.weekly_start_time),
79
- weeklyEnd: parseTimestamp(value.weekly_end_time),
58
+ weeklyStart: parsePositiveTimestamp(value.weekly_start_time),
59
+ weeklyEnd: parsePositiveTimestamp(value.weekly_end_time),
80
60
  weeklyRemainingPercent: toNumber(value.current_weekly_remaining_percent),
81
61
  weeklyTotalCount: toNumber(value.current_weekly_total_count),
82
62
  weeklyUsageCount: toNumber(value.current_weekly_usage_count),
@@ -18,10 +18,11 @@
18
18
  * (the `/usage reset` command + auto-redeem) and any out-of-band tooling can
19
19
  * share one wire contract.
20
20
  */
21
+ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
22
+ import { USER_AGENT } from "@oh-my-pi/pi-utils";
21
23
  import type { FetchImpl } from "../types";
22
24
  import { isRecord } from "../utils";
23
25
  import { normalizeCodexBaseUrl } from "./openai-codex-base-url";
24
- import { toNumber } from "./shared";
25
26
 
26
27
  const RESET_CREDITS_PATH = "wham/rate-limit-reset-credits";
27
28
  const RESET_CREDITS_CONSUME_PATH = "wham/rate-limit-reset-credits/consume";
@@ -89,7 +90,7 @@ function buildUrl(baseUrl: string | undefined, routePath: string): string {
89
90
  function buildHeaders(auth: CodexResetAuth, json: boolean): Record<string, string> {
90
91
  const headers: Record<string, string> = {
91
92
  Authorization: `Bearer ${auth.accessToken}`,
92
- "User-Agent": "OpenCode-Status-Plugin/1.0",
93
+ "User-Agent": USER_AGENT,
93
94
  };
94
95
  if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
95
96
  if (json) headers["Content-Type"] = "application/json";
@@ -1,4 +1,6 @@
1
1
  import { Buffer } from "node:buffer";
2
+ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
3
+ import { USER_AGENT } from "@oh-my-pi/pi-utils";
2
4
  import type {
3
5
  CredentialRankingContext,
4
6
  CredentialRankingStrategy,
@@ -14,7 +16,7 @@ import type {
14
16
  import { isRecord } from "../utils";
15
17
  import { normalizeCodexBaseUrl } from "./openai-codex-base-url";
16
18
  import { listCodexResetCredits } from "./openai-codex-reset";
17
- import { toNumber } from "./shared";
19
+ import { HOUR_MS } from "./shared";
18
20
 
19
21
  const CODEX_USAGE_PATH = "wham/usage";
20
22
  const JWT_AUTH_CLAIM = "https://api.openai.com/auth";
@@ -416,7 +418,7 @@ export const openaiCodexUsageProvider: UsageProvider = {
416
418
 
417
419
  const headers: Record<string, string> = {
418
420
  Authorization: `Bearer ${accessToken}`,
419
- "User-Agent": "OpenCode-Status-Plugin/1.0",
421
+ "User-Agent": USER_AGENT,
420
422
  };
421
423
  if (accountId) {
422
424
  headers["ChatGPT-Account-Id"] = accountId;
@@ -558,8 +560,6 @@ export const openaiCodexUsageProvider: UsageProvider = {
558
560
  },
559
561
  };
560
562
 
561
- const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
562
-
563
563
  // A Codex request gates only on the chat windows it actually consumes. A
564
564
  // "-spark" model spends the separate Spark meter; every other Codex model spends
565
565
  // the 5h/weekly chat windows. Scoping the gating set this way keeps an exhausted
@@ -619,7 +619,7 @@ export const codexRankingStrategy: CredentialRankingStrategy = {
619
619
  windowId === "5h" ||
620
620
  (typeof durationMs === "number" &&
621
621
  Number.isFinite(durationMs) &&
622
- Math.abs(durationMs - FIVE_HOUR_MS) <= 60_000);
622
+ Math.abs(durationMs - 5 * HOUR_MS) <= 60_000);
623
623
  if (!isFiveHourWindow) return false;
624
624
  const usedFraction = primary.amount.usedFraction;
625
625
  return typeof usedFraction === "number" && Number.isFinite(usedFraction) && usedFraction === 0;
@@ -1,8 +1,7 @@
1
1
  import type { UsageCostHistoryEntry, UsageLimit, UsageProvider, UsageWindow } from "../usage";
2
+ import { DAY_MS, HOUR_MS } from "./shared";
2
3
 
3
4
  const OPENCODE_GO_PROVIDER = "opencode-go";
4
- const HOUR_MS = 60 * 60 * 1000;
5
- const DAY_MS = 24 * HOUR_MS;
6
5
  const OPENCODE_GO_LIMITS = [
7
6
  { id: "rolling-5h", label: "5 Hour", durationMs: 5 * HOUR_MS, limitUsd: 12 },
8
7
  { id: "weekly", label: "Weekly", durationMs: 7 * DAY_MS, limitUsd: 30 },
@@ -1,10 +1,33 @@
1
- export const toNumber = (value: unknown): number | undefined => {
2
- if (typeof value === "number" && Number.isFinite(value)) return value;
3
- if (typeof value === "string") {
4
- const trimmed = value.trim();
5
- if (!trimmed) return undefined;
6
- const parsed = Number(trimmed);
7
- if (Number.isFinite(parsed)) return parsed;
8
- }
9
- return undefined;
10
- };
1
+ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
2
+ import type { UsageStatus } from "../usage";
3
+
4
+ /** Milliseconds in one hour. */
5
+ export const HOUR_MS = 60 * 60 * 1000;
6
+
7
+ /** Milliseconds in one day. */
8
+ export const DAY_MS = 24 * HOUR_MS;
9
+
10
+ /** Milliseconds in one seven-day week. */
11
+ export const WEEK_MS = 7 * DAY_MS;
12
+
13
+ /** Parses a finite positive epoch timestamp, tolerating seconds or milliseconds. */
14
+ export function parsePositiveTimestamp(value: unknown): number | undefined {
15
+ const parsed = toNumber(value);
16
+ if (parsed === undefined || parsed <= 0) return undefined;
17
+ return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
18
+ }
19
+
20
+ /** Parses an ISO timestamp into epoch milliseconds. */
21
+ export function parseIsoTimestamp(value: unknown): number | undefined {
22
+ if (typeof value !== "string" || !value) return undefined;
23
+ const parsed = Date.parse(value);
24
+ return Number.isFinite(parsed) ? parsed : undefined;
25
+ }
26
+
27
+ /** Maps a used fraction to the standard quota status thresholds. */
28
+ export function usageStatus(usedFraction: number | undefined): UsageStatus {
29
+ if (usedFraction === undefined) return "unknown";
30
+ if (usedFraction >= 1) return "exhausted";
31
+ if (usedFraction >= 0.9) return "warning";
32
+ return "ok";
33
+ }
@@ -9,10 +9,9 @@ import type {
9
9
  UsageStatus,
10
10
  UsageWindow,
11
11
  } from "../usage";
12
+ import { HOUR_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
12
13
 
13
14
  const QUOTAS_URL = "https://api.synthetic.new/v2/quotas";
14
- const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
15
- const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
16
15
 
17
16
  function parseDollarAmount(value: unknown): number | undefined {
18
17
  if (typeof value !== "string") return undefined;
@@ -21,12 +20,6 @@ function parseDollarAmount(value: unknown): number | undefined {
21
20
  return Number.isFinite(parsed) ? parsed : undefined;
22
21
  }
23
22
 
24
- function parseIsoMs(value: unknown): number | undefined {
25
- if (typeof value !== "string" || !value) return undefined;
26
- const ms = Date.parse(value);
27
- return Number.isFinite(ms) ? ms : undefined;
28
- }
29
-
30
23
  function buildUsageAmount(args: {
31
24
  used: number | undefined;
32
25
  limit: number | undefined;
@@ -61,7 +54,7 @@ function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["pro
61
54
  const remaining = typeof raw.remaining === "number" ? raw.remaining : undefined;
62
55
  const max = typeof raw.max === "number" ? raw.max : undefined;
63
56
  const limited = raw.limited === true;
64
- const nextTickAt = parseIsoMs(raw.nextTickAt);
57
+ const nextTickAt = parseIsoTimestamp(raw.nextTickAt);
65
58
  const tickPercent = typeof raw.tickPercent === "number" ? raw.tickPercent : undefined;
66
59
 
67
60
  if (remaining === undefined && max === undefined) return null;
@@ -71,7 +64,7 @@ function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["pro
71
64
  const window: UsageWindow = {
72
65
  id: "5h",
73
66
  label: regenPercent !== undefined ? `5h · regen ${regenPercent}%/tick` : "5h",
74
- durationMs: FIVE_HOUR_MS,
67
+ durationMs: 5 * HOUR_MS,
75
68
  ...(nextTickAt !== undefined ? { resetsAt: nextTickAt, resetLabel: "tick" } : {}),
76
69
  };
77
70
  const amount = buildUsageAmount({
@@ -97,7 +90,7 @@ function parseWeeklyTokenLimit(raw: unknown, provider: UsageFetchParams["provide
97
90
  const remainingCredits = parseDollarAmount(raw.remainingCredits);
98
91
  const maxCredits = parseDollarAmount(raw.maxCredits);
99
92
  const percentRemaining = typeof raw.percentRemaining === "number" ? raw.percentRemaining : undefined;
100
- const nextRegenAt = parseIsoMs(raw.nextRegenAt);
93
+ const nextRegenAt = parseIsoTimestamp(raw.nextRegenAt);
101
94
 
102
95
  if (remainingCredits === undefined && maxCredits === undefined) return null;
103
96
 
@@ -10,11 +10,11 @@ import type {
10
10
  UsageWindow,
11
11
  } from "../usage";
12
12
  import { isRecord } from "../utils";
13
+ import { HOUR_MS } from "./shared";
13
14
 
14
15
  const UMANS_PROVIDER = "umans";
15
16
  const DEFAULT_ENDPOINT = "https://api.code.umans.ai";
16
17
  const USAGE_PATH = "/v1/usage";
17
- const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
18
18
 
19
19
  /** Umans `GET /v1/usage` response (subset; extras ignored). */
20
20
  interface UmansUsagePayload {
@@ -89,7 +89,7 @@ function buildRequestsLimit(payload: UmansUsagePayload, provider: string): Usage
89
89
  const window: UsageWindow = {
90
90
  id: "5h",
91
91
  label: "rolling 5h",
92
- durationMs: windowSeconds ? windowSeconds * 1000 : FIVE_HOURS_MS,
92
+ durationMs: windowSeconds ? windowSeconds * 1000 : 5 * HOUR_MS,
93
93
  };
94
94
  return {
95
95
  id: "umans:requests",
@@ -9,6 +9,7 @@
9
9
  * product and must never be sent here.
10
10
  */
11
11
 
12
+ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
12
13
  import {
13
14
  buildXAICliBillingUrl,
14
15
  extractXAIAccessTokenSubject,
@@ -22,15 +23,12 @@ import type {
22
23
  UsageLimit,
23
24
  UsageProvider,
24
25
  UsageReport,
25
- UsageStatus,
26
26
  UsageWindow,
27
27
  } from "../usage";
28
28
  import { isRecord } from "../utils";
29
- import { toNumber } from "./shared";
29
+ import { DAY_MS, parseIsoTimestamp, usageStatus, WEEK_MS } from "./shared";
30
30
 
31
31
  const PROVIDER_ID = "xai-oauth";
32
- const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
33
- const DAY_MS = 24 * 60 * 60 * 1000;
34
32
  const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing";
35
33
 
36
34
  interface XaiBillingPeriod {
@@ -71,11 +69,6 @@ interface XaiMonthlyBillingConfig {
71
69
 
72
70
  type XaiBillingConfig = XaiWeeklyBillingConfig | XaiMonthlyBillingConfig;
73
71
 
74
- function parseIsoMs(value: string): number | undefined {
75
- const parsed = Date.parse(value);
76
- return Number.isFinite(parsed) ? parsed : undefined;
77
- }
78
-
79
72
  function parsePercent(value: unknown): number | undefined {
80
73
  const percent = toNumber(value);
81
74
  return percent !== undefined && percent >= 0 && percent <= 100 ? percent : undefined;
@@ -99,12 +92,6 @@ function buildPercentAmount(usagePercent: number): UsageAmount {
99
92
  };
100
93
  }
101
94
 
102
- function buildUsageStatus(usedFraction: number): UsageStatus {
103
- if (usedFraction >= 1) return "exhausted";
104
- if (usedFraction >= 0.9) return "warning";
105
- return "ok";
106
- }
107
-
108
95
  function slugifyProduct(product: string): string {
109
96
  return product
110
97
  .trim()
@@ -118,13 +105,13 @@ function buildPeriodWindow(period: XaiBillingPeriod): UsageWindow {
118
105
  id: "1w",
119
106
  label: "Weekly",
120
107
  durationMs: WEEK_MS,
121
- resetsAt: parseIsoMs(period.end),
108
+ resetsAt: parseIsoTimestamp(period.end),
122
109
  };
123
110
  }
124
111
 
125
112
  function buildMonthlyWindow(periodStart: string, periodEnd: string): UsageWindow | undefined {
126
- const startMs = parseIsoMs(periodStart);
127
- const endMs = parseIsoMs(periodEnd);
113
+ const startMs = parseIsoTimestamp(periodStart);
114
+ const endMs = parseIsoTimestamp(periodEnd);
128
115
  if (startMs === undefined || endMs === undefined || endMs <= startMs) return undefined;
129
116
  // Real calendar months vary; use the observed period length from the API.
130
117
  const durationMs = endMs - startMs;
@@ -140,8 +127,8 @@ function buildMonthlyWindow(periodStart: string, periodEnd: string): UsageWindow
140
127
  function parseWeeklyBillingConfig(raw: Record<string, unknown>): XaiWeeklyBillingConfig | null {
141
128
  if (!isRecord(raw.currentPeriod)) return null;
142
129
 
143
- const start = typeof raw.currentPeriod.start === "string" ? parseIsoMs(raw.currentPeriod.start) : undefined;
144
- const end = typeof raw.currentPeriod.end === "string" ? parseIsoMs(raw.currentPeriod.end) : undefined;
130
+ const start = typeof raw.currentPeriod.start === "string" ? parseIsoTimestamp(raw.currentPeriod.start) : undefined;
131
+ const end = typeof raw.currentPeriod.end === "string" ? parseIsoTimestamp(raw.currentPeriod.end) : undefined;
145
132
  const type = typeof raw.currentPeriod.type === "string" ? raw.currentPeriod.type : "";
146
133
  // Keep recently-ended weekly windows so /usage still renders across period
147
134
  // rollover while the billing API is mid-refresh. Reject only inverted ranges
@@ -182,8 +169,8 @@ function parseWeeklyBillingConfig(raw: Record<string, unknown>): XaiWeeklyBillin
182
169
  function parseMonthlyBillingConfig(raw: Record<string, unknown>): XaiMonthlyBillingConfig | null {
183
170
  const periodStart = typeof raw.billingPeriodStart === "string" ? raw.billingPeriodStart : "";
184
171
  const periodEnd = typeof raw.billingPeriodEnd === "string" ? raw.billingPeriodEnd : "";
185
- const startMs = parseIsoMs(periodStart);
186
- const endMs = parseIsoMs(periodEnd);
172
+ const startMs = parseIsoTimestamp(periodStart);
173
+ const endMs = parseIsoTimestamp(periodEnd);
187
174
  if (!periodStart || !periodEnd || startMs === undefined || endMs === undefined || endMs <= startMs) {
188
175
  return null;
189
176
  }
@@ -227,7 +214,7 @@ function buildOnDemandLimit(
227
214
  remainingFraction: 1 - usedFraction,
228
215
  unit: "unknown",
229
216
  },
230
- status: buildUsageStatus(usedFraction),
217
+ status: usageStatus(usedFraction),
231
218
  };
232
219
  }
233
220
 
@@ -248,7 +235,7 @@ function buildLimits(config: XaiBillingConfig, accountId: string | undefined): U
248
235
  scope,
249
236
  window,
250
237
  amount: overall,
251
- status: buildUsageStatus(overall.usedFraction ?? 0),
238
+ status: usageStatus(overall.usedFraction ?? 0),
252
239
  },
253
240
  ];
254
241
 
@@ -262,7 +249,7 @@ function buildLimits(config: XaiBillingConfig, accountId: string | undefined): U
262
249
  scope,
263
250
  window,
264
251
  amount,
265
- status: buildUsageStatus(amount.usedFraction ?? 0),
252
+ status: usageStatus(amount.usedFraction ?? 0),
266
253
  });
267
254
  }
268
255
  const onDemand = buildOnDemandLimit(config.onDemandCap, config.onDemandUsed, accountId);
@@ -293,7 +280,7 @@ function buildLimits(config: XaiBillingConfig, accountId: string | undefined): U
293
280
  // xAI does not label the unit; amounts match the dashboard quota points.
294
281
  unit: "unknown",
295
282
  },
296
- status: buildUsageStatus(usedFraction),
283
+ status: usageStatus(usedFraction),
297
284
  },
298
285
  ];
299
286
  const onDemand = buildOnDemandLimit(config.onDemandCap, config.onDemandUsed, accountId);
package/src/usage/zai.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { toNumber } from "@oh-my-pi/pi-catalog/utils";
2
+ import { USER_AGENT } from "@oh-my-pi/pi-utils";
2
3
  import type {
3
4
  CredentialRankingStrategy,
4
5
  UsageAmount,
@@ -11,14 +12,11 @@ import type {
11
12
  UsageWindow,
12
13
  } from "../usage";
13
14
  import { isRecord } from "../utils";
15
+ import { DAY_MS, HOUR_MS, WEEK_MS } from "./shared";
14
16
 
15
17
  const DEFAULT_ENDPOINT = "https://api.z.ai";
16
18
  const QUOTA_PATH = "/api/monitor/usage/quota/limit";
17
19
  const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
18
- const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
19
- const HOUR_MS = 60 * 60 * 1000;
20
- const DAY_MS = 24 * HOUR_MS;
21
- const WEEK_MS = 7 * DAY_MS;
22
20
  const MONTH_MS = 30 * DAY_MS;
23
21
 
24
22
  interface ZaiUsageDetail {
@@ -188,7 +186,7 @@ function requestQuotaLabel(parsed: ZaiUsageLimitItem): string {
188
186
  }
189
187
 
190
188
  function buildModelUsageUrl(baseUrl: string, now: Date): string {
191
- const start = new Date(now.getTime() - SEVEN_DAYS_MS);
189
+ const start = new Date(now.getTime() - WEEK_MS);
192
190
  const startTime = formatDate(start);
193
191
  const endTime = formatDate(now);
194
192
  return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;
@@ -231,7 +229,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
231
229
  const headers: Record<string, string> = {
232
230
  Authorization: token,
233
231
  "Content-Type": "application/json",
234
- "User-Agent": "OpenCode-Status-Plugin/1.0",
232
+ "User-Agent": USER_AGENT,
235
233
  };
236
234
 
237
235
  let payload: ZaiQuotaPayload | null = null;
@@ -78,7 +78,45 @@ export function hasConfiguredAwsProfile(profile?: string): boolean {
78
78
  const configPath = $env.AWS_CONFIG_FILE || path.join(os.homedir(), ".aws", "config");
79
79
  const credentialsIni = readAwsIniSync(credentialsPath);
80
80
  const configIni = shouldLoadAwsSharedConfig(profile) ? readAwsIniSync(configPath) : undefined;
81
- const merged = { ...(configIni?.[selectedProfile] ?? {}), ...(credentialsIni?.[selectedProfile] ?? {}) };
81
+ return profileHasCredentialSource(selectedProfile, credentialsIni, configIni, new Set());
82
+ }
83
+
84
+ /**
85
+ * Whether a profile terminates in a usable credential source. Mirrors the
86
+ * resolver's per-profile dispatch (static keys, SSO, `credential_process`,
87
+ * `role_arn` chaining) so the availability probe never diverges from what
88
+ * `resolveAwsCredentials` can actually resolve. `role_arn` chains follow
89
+ * `source_profile` recursively (cycle-guarded by `seen`); MFA-gated roles are
90
+ * treated as unusable because non-interactive resolution cannot supply a token.
91
+ */
92
+ function profileHasCredentialSource(
93
+ profile: string,
94
+ credentialsIni: AwsIniFile | undefined,
95
+ configIni: AwsIniFile | undefined,
96
+ seen: Set<string>,
97
+ ): boolean {
98
+ if (seen.has(profile)) return false;
99
+ seen.add(profile);
100
+ const merged = { ...(configIni?.[profile] ?? {}), ...(credentialsIni?.[profile] ?? {}) };
101
+ if (merged.role_arn) {
102
+ if (merged.web_identity_token_file) return true;
103
+ if (merged.mfa_serial) return false;
104
+ if (merged.credential_source) {
105
+ switch (merged.credential_source) {
106
+ case "Environment":
107
+ return !!($env.AWS_ACCESS_KEY_ID && $env.AWS_SECRET_ACCESS_KEY);
108
+ case "EcsContainer":
109
+ return !!($env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || $env.AWS_CONTAINER_CREDENTIALS_FULL_URI);
110
+ case "Ec2InstanceMetadata":
111
+ return $env.AWS_EC2_METADATA_DISABLED?.toLowerCase() !== "true";
112
+ default:
113
+ return false;
114
+ }
115
+ }
116
+ if (merged.source_profile)
117
+ return profileHasCredentialSource(merged.source_profile, credentialsIni, configIni, seen);
118
+ return false;
119
+ }
82
120
  if (merged.aws_access_key_id && merged.aws_secret_access_key) return true;
83
121
  if (merged.credential_process) return true;
84
122
  if (!merged.sso_account_id || !merged.sso_role_name) return false;
@@ -59,6 +59,24 @@ export const kCursorExecResolved = Symbol("provider.block.cursorExecResolved");
59
59
  /** Carries the resolved marker without exposing a string-keyed property. */
60
60
  export type CursorExecResolvedCarrier = object & { [kCursorExecResolved]?: true };
61
61
 
62
+ /** True when a toolCall block was already executed by Cursor's exec channel. */
63
+ export function isCursorExecResolved(block: CursorExecResolvedCarrier | null | undefined): boolean {
64
+ return block?.[kCursorExecResolved] === true;
65
+ }
66
+
67
+ /**
68
+ * Copy {@link kCursorExecResolved} onto a cloned/projected toolCall block.
69
+ *
70
+ * Stream projectors (owned/in-band dialect, leaked-thinking heal) rebuild
71
+ * toolCall objects field-by-field. Dropping this marker lets `agent-loop.ts`
72
+ * re-execute a call Cursor already settled — duplicate toolResults and a
73
+ * second bash/write/delete. Partial-JSON is already copied explicitly; this
74
+ * marker is the other load-bearing symbol that must survive the same way.
75
+ */
76
+ export function copyCursorExecResolved(target: CursorExecResolvedCarrier, source: CursorExecResolvedCarrier): void {
77
+ if (source[kCursorExecResolved] === true) target[kCursorExecResolved] = true;
78
+ }
79
+
62
80
  /**
63
81
  * Marks a text block synthesized by cross-model thinking demotion in
64
82
  * `transformMessages`. Converters that flatten adjacent text blocks into one
@@ -36,6 +36,7 @@ import type {
36
36
  } from "../types";
37
37
  import {
38
38
  clearStreamingPartialJson,
39
+ copyCursorExecResolved,
39
40
  getStreamingPartialJson,
40
41
  type StreamingPartialJsonCarrier,
41
42
  setStreamingPartialJson,
@@ -49,6 +50,7 @@ function cloneToolCall(source: StreamingToolCall): StreamingToolCall {
49
50
  const block: StreamingToolCall = { ...source, arguments: source.arguments };
50
51
  const partialJson = getStreamingPartialJson(source);
51
52
  if (partialJson !== undefined) setStreamingPartialJson(block, partialJson);
53
+ copyCursorExecResolved(block, source);
52
54
  return block;
53
55
  }
54
56
 
@@ -57,6 +59,7 @@ function syncToolCall(target: StreamingToolCall, source: StreamingToolCall): voi
57
59
  const partialJson = getStreamingPartialJson(source);
58
60
  if (partialJson === undefined) clearStreamingPartialJson(target);
59
61
  else setStreamingPartialJson(target, partialJson);
62
+ copyCursorExecResolved(target, source);
60
63
  }
61
64
 
62
65
  /**
@@ -1,10 +1,10 @@
1
- import packageJson from "../../package.json" with { type: "json" };
1
+ import { USER_AGENT } from "@oh-my-pi/pi-utils";
2
2
 
3
3
  export function getOpenRouterHeaders(): Record<string, string> {
4
4
  return {
5
- "User-Agent": `Oh-My-Pi/${packageJson.version}`,
5
+ "User-Agent": USER_AGENT,
6
6
  "HTTP-Referer": "https://omp.sh/",
7
- "X-OpenRouter-Title": "Oh-My-Pi",
7
+ "X-OpenRouter-Title": "omp",
8
8
  "X-OpenRouter-Categories": "cli-agent",
9
9
  "X-OpenRouter-Cache": "true",
10
10
  "X-OpenRouter-Cache-TTL": "3600",