@slkiser/opencode-quota 4.8.1 → 4.9.0

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 (233) hide show
  1. package/README.md +17 -18
  2. package/dist/bin/opencode-quota.d.ts.map +1 -1
  3. package/dist/bin/opencode-quota.js +5 -3
  4. package/dist/bin/opencode-quota.js.map +1 -1
  5. package/dist/lib/accounting-format.d.ts +54 -0
  6. package/dist/lib/accounting-format.d.ts.map +1 -0
  7. package/dist/lib/accounting-format.js +271 -0
  8. package/dist/lib/accounting-format.js.map +1 -0
  9. package/dist/lib/anthropic.d.ts +2 -0
  10. package/dist/lib/anthropic.d.ts.map +1 -1
  11. package/dist/lib/anthropic.js +79 -19
  12. package/dist/lib/anthropic.js.map +1 -1
  13. package/dist/lib/cli-show.d.ts.map +1 -1
  14. package/dist/lib/cli-show.js +1 -0
  15. package/dist/lib/cli-show.js.map +1 -1
  16. package/dist/lib/config.d.ts +10 -1
  17. package/dist/lib/config.d.ts.map +1 -1
  18. package/dist/lib/config.js +35 -3
  19. package/dist/lib/config.js.map +1 -1
  20. package/dist/lib/copilot.d.ts +2 -0
  21. package/dist/lib/copilot.d.ts.map +1 -1
  22. package/dist/lib/copilot.js +38 -0
  23. package/dist/lib/copilot.js.map +1 -1
  24. package/dist/lib/deepseek.d.ts +12 -12
  25. package/dist/lib/deepseek.d.ts.map +1 -1
  26. package/dist/lib/deepseek.js +33 -29
  27. package/dist/lib/deepseek.js.map +1 -1
  28. package/dist/lib/display-sanitize.d.ts.map +1 -1
  29. package/dist/lib/display-sanitize.js +76 -22
  30. package/dist/lib/display-sanitize.js.map +1 -1
  31. package/dist/lib/entries.d.ts +107 -23
  32. package/dist/lib/entries.d.ts.map +1 -1
  33. package/dist/lib/entries.js +37 -1
  34. package/dist/lib/entries.js.map +1 -1
  35. package/dist/lib/format.d.ts +1 -0
  36. package/dist/lib/format.d.ts.map +1 -1
  37. package/dist/lib/format.js +84 -19
  38. package/dist/lib/format.js.map +1 -1
  39. package/dist/lib/glm-coding-plan.d.ts.map +1 -1
  40. package/dist/lib/glm-coding-plan.js +4 -2
  41. package/dist/lib/glm-coding-plan.js.map +1 -1
  42. package/dist/lib/google-agy.d.ts +2 -0
  43. package/dist/lib/google-agy.d.ts.map +1 -1
  44. package/dist/lib/google-agy.js +29 -14
  45. package/dist/lib/google-agy.js.map +1 -1
  46. package/dist/lib/google-gemini-cli.d.ts +2 -0
  47. package/dist/lib/google-gemini-cli.d.ts.map +1 -1
  48. package/dist/lib/google-gemini-cli.js +29 -3
  49. package/dist/lib/google-gemini-cli.js.map +1 -1
  50. package/dist/lib/google-token-cache.d.ts +5 -8
  51. package/dist/lib/google-token-cache.d.ts.map +1 -1
  52. package/dist/lib/google-token-cache.js +30 -118
  53. package/dist/lib/google-token-cache.js.map +1 -1
  54. package/dist/lib/google.d.ts +2 -0
  55. package/dist/lib/google.d.ts.map +1 -1
  56. package/dist/lib/google.js +31 -6
  57. package/dist/lib/google.js.map +1 -1
  58. package/dist/lib/grouped-entry-normalization.d.ts.map +1 -1
  59. package/dist/lib/grouped-entry-normalization.js +53 -20
  60. package/dist/lib/grouped-entry-normalization.js.map +1 -1
  61. package/dist/lib/minimax-auth.d.ts +1 -1
  62. package/dist/lib/minimax-auth.js +1 -1
  63. package/dist/lib/nanogpt.d.ts +5 -5
  64. package/dist/lib/nanogpt.d.ts.map +1 -1
  65. package/dist/lib/nanogpt.js +59 -23
  66. package/dist/lib/nanogpt.js.map +1 -1
  67. package/dist/lib/openai.d.ts +4 -0
  68. package/dist/lib/openai.d.ts.map +1 -1
  69. package/dist/lib/openai.js +22 -3
  70. package/dist/lib/openai.js.map +1 -1
  71. package/dist/lib/opencode-go-auth.js +1 -1
  72. package/dist/lib/opencode-go-auth.js.map +1 -1
  73. package/dist/lib/opencode-go.d.ts.map +1 -1
  74. package/dist/lib/opencode-go.js +6 -1
  75. package/dist/lib/opencode-go.js.map +1 -1
  76. package/dist/lib/opencode-zen-config.d.ts +0 -1
  77. package/dist/lib/opencode-zen-config.d.ts.map +1 -1
  78. package/dist/lib/opencode-zen-config.js +0 -22
  79. package/dist/lib/opencode-zen-config.js.map +1 -1
  80. package/dist/lib/opencode-zen.d.ts +3 -0
  81. package/dist/lib/opencode-zen.d.ts.map +1 -1
  82. package/dist/lib/opencode-zen.js +18 -1
  83. package/dist/lib/opencode-zen.js.map +1 -1
  84. package/dist/lib/openrouter.d.ts +2 -0
  85. package/dist/lib/openrouter.d.ts.map +1 -1
  86. package/dist/lib/openrouter.js +10 -0
  87. package/dist/lib/openrouter.js.map +1 -1
  88. package/dist/lib/provider-metadata.d.ts +3 -307
  89. package/dist/lib/provider-metadata.d.ts.map +1 -1
  90. package/dist/lib/provider-metadata.js +18 -316
  91. package/dist/lib/provider-metadata.js.map +1 -1
  92. package/dist/lib/provider-registration.d.ts +325 -0
  93. package/dist/lib/provider-registration.d.ts.map +1 -0
  94. package/dist/lib/provider-registration.js +338 -0
  95. package/dist/lib/provider-registration.js.map +1 -0
  96. package/dist/lib/quota-accounting-projection.d.ts +7 -0
  97. package/dist/lib/quota-accounting-projection.d.ts.map +1 -0
  98. package/dist/lib/quota-accounting-projection.js +239 -0
  99. package/dist/lib/quota-accounting-projection.js.map +1 -0
  100. package/dist/lib/quota-command-format.d.ts +3 -2
  101. package/dist/lib/quota-command-format.d.ts.map +1 -1
  102. package/dist/lib/quota-command-format.js +35 -9
  103. package/dist/lib/quota-command-format.js.map +1 -1
  104. package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
  105. package/dist/lib/quota-dialog-commands.js +1 -0
  106. package/dist/lib/quota-dialog-commands.js.map +1 -1
  107. package/dist/lib/quota-export.d.ts +2 -1
  108. package/dist/lib/quota-export.d.ts.map +1 -1
  109. package/dist/lib/quota-export.js +36 -10
  110. package/dist/lib/quota-export.js.map +1 -1
  111. package/dist/lib/quota-render-data.d.ts +0 -1
  112. package/dist/lib/quota-render-data.d.ts.map +1 -1
  113. package/dist/lib/quota-render-data.js +43 -155
  114. package/dist/lib/quota-render-data.js.map +1 -1
  115. package/dist/lib/quota-reset-notifications.d.ts.map +1 -1
  116. package/dist/lib/quota-reset-notifications.js +3 -0
  117. package/dist/lib/quota-reset-notifications.js.map +1 -1
  118. package/dist/lib/quota-state-codec.d.ts +16 -0
  119. package/dist/lib/quota-state-codec.d.ts.map +1 -0
  120. package/dist/lib/quota-state-codec.js +409 -0
  121. package/dist/lib/quota-state-codec.js.map +1 -0
  122. package/dist/lib/quota-state.d.ts +2 -11
  123. package/dist/lib/quota-state.d.ts.map +1 -1
  124. package/dist/lib/quota-state.js +165 -304
  125. package/dist/lib/quota-state.js.map +1 -1
  126. package/dist/lib/quota-status.d.ts.map +1 -1
  127. package/dist/lib/quota-status.js +14 -7
  128. package/dist/lib/quota-status.js.map +1 -1
  129. package/dist/lib/quota-telemetry.d.ts.map +1 -1
  130. package/dist/lib/quota-telemetry.js +7 -3
  131. package/dist/lib/quota-telemetry.js.map +1 -1
  132. package/dist/lib/quota-toast-runtime.d.ts +60 -0
  133. package/dist/lib/quota-toast-runtime.d.ts.map +1 -0
  134. package/dist/lib/quota-toast-runtime.js +765 -0
  135. package/dist/lib/quota-toast-runtime.js.map +1 -0
  136. package/dist/lib/resolved-auth-identity.d.ts +36 -0
  137. package/dist/lib/resolved-auth-identity.d.ts.map +1 -0
  138. package/dist/lib/resolved-auth-identity.js +193 -0
  139. package/dist/lib/resolved-auth-identity.js.map +1 -0
  140. package/dist/lib/scoped-update-migration.d.ts +101 -0
  141. package/dist/lib/scoped-update-migration.d.ts.map +1 -0
  142. package/dist/lib/scoped-update-migration.js +348 -0
  143. package/dist/lib/scoped-update-migration.js.map +1 -0
  144. package/dist/lib/scoped-update.d.ts +8 -0
  145. package/dist/lib/scoped-update.d.ts.map +1 -1
  146. package/dist/lib/scoped-update.js +263 -50
  147. package/dist/lib/scoped-update.js.map +1 -1
  148. package/dist/lib/toast-format-grouped.d.ts +1 -0
  149. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  150. package/dist/lib/toast-format-grouped.js +111 -24
  151. package/dist/lib/toast-format-grouped.js.map +1 -1
  152. package/dist/lib/tui-compact-format.d.ts +1 -0
  153. package/dist/lib/tui-compact-format.d.ts.map +1 -1
  154. package/dist/lib/tui-compact-format.js +136 -24
  155. package/dist/lib/tui-compact-format.js.map +1 -1
  156. package/dist/lib/tui-panel-state.d.ts +2 -0
  157. package/dist/lib/tui-panel-state.d.ts.map +1 -1
  158. package/dist/lib/tui-panel-state.js.map +1 -1
  159. package/dist/lib/tui-runtime.d.ts.map +1 -1
  160. package/dist/lib/tui-runtime.js +84 -10
  161. package/dist/lib/tui-runtime.js.map +1 -1
  162. package/dist/lib/tui-sidebar-format.d.ts +1 -1
  163. package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
  164. package/dist/lib/tui-sidebar-format.js +1 -0
  165. package/dist/lib/tui-sidebar-format.js.map +1 -1
  166. package/dist/lib/types.d.ts +7 -1
  167. package/dist/lib/types.d.ts.map +1 -1
  168. package/dist/lib/types.js +1 -0
  169. package/dist/lib/types.js.map +1 -1
  170. package/dist/lib/xai.d.ts +2 -0
  171. package/dist/lib/xai.d.ts.map +1 -1
  172. package/dist/lib/xai.js +10 -0
  173. package/dist/lib/xai.js.map +1 -1
  174. package/dist/plugin.d.ts.map +1 -1
  175. package/dist/plugin.js +27 -720
  176. package/dist/plugin.js.map +1 -1
  177. package/dist/providers/accounting-decimal.d.ts +2 -0
  178. package/dist/providers/accounting-decimal.d.ts.map +1 -0
  179. package/dist/providers/accounting-decimal.js +23 -0
  180. package/dist/providers/accounting-decimal.js.map +1 -0
  181. package/dist/providers/cache-policies.d.ts +64 -0
  182. package/dist/providers/cache-policies.d.ts.map +1 -0
  183. package/dist/providers/cache-policies.js +197 -0
  184. package/dist/providers/cache-policies.js.map +1 -0
  185. package/dist/providers/cursor.d.ts.map +1 -1
  186. package/dist/providers/cursor.js +73 -58
  187. package/dist/providers/cursor.js.map +1 -1
  188. package/dist/providers/deepseek.d.ts +2 -2
  189. package/dist/providers/deepseek.d.ts.map +1 -1
  190. package/dist/providers/deepseek.js +64 -33
  191. package/dist/providers/deepseek.js.map +1 -1
  192. package/dist/providers/google-agy.js +2 -2
  193. package/dist/providers/google-agy.js.map +1 -1
  194. package/dist/providers/google-gemini-cli.d.ts.map +1 -1
  195. package/dist/providers/google-gemini-cli.js +1 -1
  196. package/dist/providers/google-gemini-cli.js.map +1 -1
  197. package/dist/providers/kilo.d.ts.map +1 -1
  198. package/dist/providers/kilo.js +56 -29
  199. package/dist/providers/kilo.js.map +1 -1
  200. package/dist/providers/mimo.d.ts.map +1 -1
  201. package/dist/providers/mimo.js +37 -15
  202. package/dist/providers/mimo.js.map +1 -1
  203. package/dist/providers/minimax-coding-plan.d.ts +8 -7
  204. package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
  205. package/dist/providers/minimax-coding-plan.js +97 -83
  206. package/dist/providers/minimax-coding-plan.js.map +1 -1
  207. package/dist/providers/nanogpt.d.ts.map +1 -1
  208. package/dist/providers/nanogpt.js +72 -51
  209. package/dist/providers/nanogpt.js.map +1 -1
  210. package/dist/providers/openai.d.ts.map +1 -1
  211. package/dist/providers/openai.js +0 -3
  212. package/dist/providers/openai.js.map +1 -1
  213. package/dist/providers/opencode-go.d.ts.map +1 -1
  214. package/dist/providers/opencode-go.js +3 -4
  215. package/dist/providers/opencode-go.js.map +1 -1
  216. package/dist/providers/opencode-zen.d.ts.map +1 -1
  217. package/dist/providers/opencode-zen.js +78 -17
  218. package/dist/providers/opencode-zen.js.map +1 -1
  219. package/dist/providers/quota-providers.d.ts.map +1 -1
  220. package/dist/providers/quota-providers.js +31 -23
  221. package/dist/providers/quota-providers.js.map +1 -1
  222. package/dist/providers/registry.d.ts +3 -2
  223. package/dist/providers/registry.d.ts.map +1 -1
  224. package/dist/providers/registry.js +41 -31
  225. package/dist/providers/registry.js.map +1 -1
  226. package/dist/providers/result-helpers.d.ts +10 -5
  227. package/dist/providers/result-helpers.d.ts.map +1 -1
  228. package/dist/providers/result-helpers.js +3 -3
  229. package/dist/providers/result-helpers.js.map +1 -1
  230. package/dist/tui.d.ts.map +1 -1
  231. package/dist/tui.js +11 -3
  232. package/dist/tui.tsx +16 -7
  233. package/package.json +1 -1
@@ -0,0 +1,765 @@
1
+ import { getProviders } from "../providers/registry.js";
2
+ import { DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS, isAlibabaModelId, resolveAlibabaCodingPlanAuthCached, } from "./alibaba-auth.js";
3
+ import { getOrFetchWithCacheControl } from "./cache.js";
4
+ import { createLoadConfigMeta } from "./config.js";
5
+ import { isCursorModelId, isCursorProviderId } from "./cursor-pricing.js";
6
+ import { sanitizeDisplayText } from "./display-sanitize.js";
7
+ import { formatQuotaRows } from "./format.js";
8
+ import { BUNDLED_MAINTAINER_ANNOUNCEMENTS, formatMaintainerAnnouncementHomeCountLine, getMaintainerAnnouncementsSummary, } from "./maintainer-announcements.js";
9
+ import { maybeRefreshPricingSnapshot, setPricingSnapshotAutoRefresh, setPricingSnapshotSelection, } from "./modelsdev-pricing.js";
10
+ import { resolveQuotaFormatStyle } from "./quota-format-style.js";
11
+ import { customQuotaProviderDefinitions, QUOTA_PROVIDERS_AGGREGATE_ID } from "./quota-providers.js";
12
+ import { collectQuotaRenderData, } from "./quota-render-data.js";
13
+ import { formatQuotaResetNotification, observeQuotaResetNotifications, } from "./quota-reset-notifications.js";
14
+ import { createQuotaRuntimeRequestContext, resolveQuotaRuntimeContext, } from "./quota-runtime-context.js";
15
+ import { isQwenCodeModelId, resolveQwenLocalPlanCached } from "./qwen-auth.js";
16
+ import { inspectTuiConfig } from "./tui-config-diagnostics.js";
17
+ import { DEFAULT_CONFIG } from "./types.js";
18
+ const DEFERRED_QUOTA_REFRESH_DELAYS_MS = [3_000, 15_000, 60_000, 300_000];
19
+ export function createQuotaToastRuntime(dependencies) {
20
+ let config = DEFAULT_CONFIG;
21
+ let configLoaded = false;
22
+ let configInFlight = null;
23
+ let configMeta = createLoadConfigMeta();
24
+ let runtimeProviders = getProviders();
25
+ let initDone = false;
26
+ const deferredQuotaRefreshes = new Map();
27
+ const detectedProviderIdsByToastCacheKey = new Map();
28
+ const maintainerAnnouncementToastFallback = {
29
+ pending: true,
30
+ inFlight: false,
31
+ };
32
+ function getDeferredQuotaRefreshDelayMs(attempts) {
33
+ const index = Math.min(Math.max(0, attempts), DEFERRED_QUOTA_REFRESH_DELAYS_MS.length - 1);
34
+ return DEFERRED_QUOTA_REFRESH_DELAYS_MS[index];
35
+ }
36
+ function clearDeferredQuotaRefresh(sessionID) {
37
+ const state = deferredQuotaRefreshes.get(sessionID);
38
+ if (state?.timer) {
39
+ clearTimeout(state.timer);
40
+ }
41
+ deferredQuotaRefreshes.delete(sessionID);
42
+ }
43
+ function clearDeferredQuotaRefreshTimer(state) {
44
+ if (!state.timer)
45
+ return;
46
+ clearTimeout(state.timer);
47
+ state.timer = null;
48
+ }
49
+ function scheduleDeferredQuotaRefresh(params) {
50
+ let state = deferredQuotaRefreshes.get(params.sessionID);
51
+ if (!state) {
52
+ state = {
53
+ sessionID: params.sessionID,
54
+ attempts: 0,
55
+ reason: params.reason,
56
+ queuedAtMs: Date.now(),
57
+ timer: null,
58
+ inFlight: false,
59
+ };
60
+ deferredQuotaRefreshes.set(params.sessionID, state);
61
+ }
62
+ else {
63
+ if (params.incrementAttempts) {
64
+ state.attempts += 1;
65
+ }
66
+ state.reason = params.reason;
67
+ clearDeferredQuotaRefreshTimer(state);
68
+ }
69
+ const delayMs = getDeferredQuotaRefreshDelayMs(state.attempts);
70
+ state.timer = setTimeout(() => {
71
+ void runDeferredQuotaRefresh(params.sessionID);
72
+ }, delayMs);
73
+ state.timer.unref?.();
74
+ void dependencies.log("Deferred quota refresh scheduled", {
75
+ sessionID: params.sessionID,
76
+ reason: params.reason,
77
+ attempts: state.attempts,
78
+ delayMs,
79
+ });
80
+ }
81
+ async function runDeferredQuotaRefresh(sessionID) {
82
+ const state = deferredQuotaRefreshes.get(sessionID);
83
+ if (!state || state.inFlight)
84
+ return;
85
+ await handleTriggerInternal({
86
+ sessionID,
87
+ trigger: "deferred.retry",
88
+ deferredRetry: true,
89
+ });
90
+ }
91
+ function isProviderEnabled(providerId) {
92
+ return config.enabledProviders === "auto" || config.enabledProviders.includes(providerId);
93
+ }
94
+ async function shouldBypassToastCacheForLiveLocalUsage(params) {
95
+ if (isProviderEnabled(QUOTA_PROVIDERS_AGGREGATE_ID) &&
96
+ customQuotaProviderDefinitions(config.quotaProviders).some((definition) => definition.mode === "local-estimate")) {
97
+ return true;
98
+ }
99
+ const currentSession = params.sessionMeta ?? (await dependencies.resolveSessionMeta(params.sessionID));
100
+ const currentModel = currentSession.modelID;
101
+ if (currentSession.providerID === "qwen-code" || isQwenCodeModelId(currentModel)) {
102
+ const plan = await resolveQwenLocalPlanCached();
103
+ return plan.state === "qwen_free" && isProviderEnabled("qwen-code");
104
+ }
105
+ if (currentSession.providerID === "alibaba-coding-plan" ||
106
+ currentSession.providerID === "alibaba" ||
107
+ isAlibabaModelId(currentModel)) {
108
+ const plan = await resolveAlibabaCodingPlanAuthCached({
109
+ maxAgeMs: DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS,
110
+ fallbackTier: "lite",
111
+ });
112
+ return plan.state === "configured" && isProviderEnabled("alibaba-coding-plan");
113
+ }
114
+ if (isCursorProviderId(currentSession.providerID) || isCursorModelId(currentModel)) {
115
+ return isProviderEnabled("cursor");
116
+ }
117
+ return false;
118
+ }
119
+ function buildToastCacheKey(params) {
120
+ const formatStyle = resolveQuotaFormatStyle(config.formatStyle);
121
+ const enabledProviders = config.enabledProviders === "auto" ? "auto" : config.enabledProviders.join(",");
122
+ const googleModels = config.googleModels.join(",");
123
+ const currentModel = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.modelID ?? "") : "";
124
+ const currentProviderID = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.providerID ?? "") : "";
125
+ const renderIdentity = JSON.stringify({
126
+ accountingDetail: config.accountingDetail,
127
+ resetTimeDecimals: config.resetTimeDecimals,
128
+ sessionTokenScope: config.sessionTokenScope,
129
+ opencodeGoWindows: config.opencodeGoWindows,
130
+ opencodeMonthlyLimit: config.opencodeMonthlyLimit,
131
+ quotaProviders: config.quotaProviders,
132
+ });
133
+ const rootIdentity = JSON.stringify(dependencies.roots());
134
+ return [
135
+ `sessionID=${params.sessionID}`,
136
+ `enabledProviders=${enabledProviders}`,
137
+ `formatStyle=${formatStyle}`,
138
+ `percentDisplayMode=${config.percentDisplayMode}`,
139
+ `layout=${JSON.stringify(config.layout)}`,
140
+ `showSessionTokens=${config.showSessionTokens ? "yes" : "no"}`,
141
+ `onlyCurrentModel=${config.onlyCurrentModel ? "yes" : "no"}`,
142
+ `currentModel=${currentModel}`,
143
+ `currentProviderID=${currentProviderID}`,
144
+ `anthropicBinaryPath=${config.anthropicBinaryPath}`,
145
+ `googleModels=${googleModels}`,
146
+ `cursorPlan=${config.cursorPlan}`,
147
+ `cursorIncludedApiUsd=${config.cursorIncludedApiUsd ?? ""}`,
148
+ `cursorBillingCycleStartDay=${config.cursorBillingCycleStartDay ?? ""}`,
149
+ `renderIdentity=${renderIdentity}`,
150
+ `rootIdentity=${rootIdentity}`,
151
+ ].join("|");
152
+ }
153
+ async function kickPricingRefresh(params) {
154
+ try {
155
+ const refreshPromise = maybeRefreshPricingSnapshot({
156
+ reason: params.reason,
157
+ snapshotSelection: config.pricingSnapshot.source,
158
+ });
159
+ const guardedRefreshPromise = refreshPromise.catch(() => undefined);
160
+ if (!params.maxWaitMs || params.maxWaitMs <= 0) {
161
+ void guardedRefreshPromise;
162
+ return;
163
+ }
164
+ await Promise.race([
165
+ guardedRefreshPromise,
166
+ new Promise((resolve) => {
167
+ setTimeout(resolve, params.maxWaitMs);
168
+ }),
169
+ ]);
170
+ }
171
+ catch (error) {
172
+ await dependencies.log("Pricing refresh failed", {
173
+ reason: params.reason,
174
+ error: error instanceof Error ? error.message : String(error),
175
+ });
176
+ }
177
+ }
178
+ function onFirstConfigLoaded() {
179
+ if (initDone)
180
+ return;
181
+ initDone = true;
182
+ if (config.enabled) {
183
+ void kickPricingRefresh({ reason: "init" });
184
+ }
185
+ dependencies.onInitialized({
186
+ configLoaded,
187
+ configSource: configMeta.source,
188
+ configPaths: configMeta.paths,
189
+ enabledProviders: config.enabledProviders,
190
+ minIntervalMs: config.minIntervalMs,
191
+ googleModels: config.googleModels,
192
+ cursorPlan: config.cursorPlan,
193
+ cursorIncludedApiUsd: config.cursorIncludedApiUsd,
194
+ cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
195
+ pricingSnapshotSource: config.pricingSnapshot.source,
196
+ pricingSnapshotAutoRefresh: config.pricingSnapshot.autoRefresh,
197
+ showOnIdle: config.showOnIdle,
198
+ showOnQuestion: config.showOnQuestion,
199
+ showOnCompact: config.showOnCompact,
200
+ showOnBothFail: config.showOnBothFail,
201
+ });
202
+ }
203
+ async function refreshConfig() {
204
+ if (configInFlight)
205
+ return configInFlight;
206
+ configInFlight = (async () => {
207
+ try {
208
+ const runtime = await resolveQuotaRuntimeContext({
209
+ client: dependencies.client,
210
+ roots: dependencies.roots(),
211
+ });
212
+ configMeta = runtime.configMeta;
213
+ config = runtime.config;
214
+ runtimeProviders = runtime.providers;
215
+ setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh);
216
+ setPricingSnapshotSelection(config.pricingSnapshot.source);
217
+ configLoaded = true;
218
+ onFirstConfigLoaded();
219
+ }
220
+ catch {
221
+ config = DEFAULT_CONFIG;
222
+ configMeta = createLoadConfigMeta();
223
+ runtimeProviders = getProviders();
224
+ setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh);
225
+ setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source);
226
+ }
227
+ finally {
228
+ configInFlight = null;
229
+ }
230
+ })();
231
+ return configInFlight;
232
+ }
233
+ async function resolvePluginRuntimeContext(params) {
234
+ return resolveQuotaRuntimeContext({
235
+ client: dependencies.client,
236
+ roots: dependencies.roots(),
237
+ config,
238
+ configMeta,
239
+ providers: runtimeProviders,
240
+ sessionID: params.sessionID,
241
+ sessionMeta: params.sessionMeta,
242
+ resolveSessionMeta: dependencies.resolveSessionMeta,
243
+ includeSessionMeta: (runtimeConfig) => runtimeConfig.onlyCurrentModel,
244
+ });
245
+ }
246
+ function emptyCollectionResult(params) {
247
+ return {
248
+ message: params.message,
249
+ cacheRenderedMessage: false,
250
+ retryable: params.retryable,
251
+ retryReason: params.retryReason,
252
+ hasQuotaRows: false,
253
+ detectedProviderIds: [],
254
+ freshProviderResults: [],
255
+ shouldReconcileDetectedProviders: false,
256
+ };
257
+ }
258
+ async function fetchQuotaMessageResult(params) {
259
+ if (!configLoaded) {
260
+ await refreshConfig();
261
+ }
262
+ if (!configLoaded) {
263
+ return emptyCollectionResult({
264
+ message: config.debug
265
+ ? formatQuotaToastDebugInfo({
266
+ trigger: params.trigger,
267
+ reason: "config load failed",
268
+ config,
269
+ configMeta,
270
+ })
271
+ : null,
272
+ retryable: true,
273
+ retryReason: "config_load_failed",
274
+ });
275
+ }
276
+ if (!config.enabled) {
277
+ return emptyCollectionResult({
278
+ message: config.debug
279
+ ? formatQuotaToastDebugInfo({
280
+ trigger: params.trigger,
281
+ reason: "disabled",
282
+ config,
283
+ configMeta,
284
+ })
285
+ : null,
286
+ retryable: false,
287
+ });
288
+ }
289
+ if (config.enabledProviders !== "auto" && config.enabledProviders.length === 0) {
290
+ return emptyCollectionResult({
291
+ message: config.debug
292
+ ? formatQuotaToastDebugInfo({
293
+ trigger: params.trigger,
294
+ reason: "enabledProviders empty",
295
+ config,
296
+ configMeta,
297
+ })
298
+ : null,
299
+ retryable: false,
300
+ });
301
+ }
302
+ const runtime = await resolvePluginRuntimeContext({
303
+ sessionID: params.sessionID,
304
+ sessionMeta: params.sessionMeta,
305
+ });
306
+ const collection = await collectQuotaToastMessage({
307
+ trigger: params.trigger,
308
+ runtime,
309
+ bypassProviderCache: params.bypassProviderCache,
310
+ });
311
+ if (collection.shouldReconcileDetectedProviders) {
312
+ await dependencies.reconcileDetectedProviders(collection.detectedProviderIds);
313
+ }
314
+ if (runtime.config.showSessionTokens) {
315
+ dependencies.setSessionTokenError(collection.sessionTokenError);
316
+ }
317
+ return collection;
318
+ }
319
+ async function reconcileDeferredQuotaRefresh(params) {
320
+ const existing = deferredQuotaRefreshes.get(params.sessionID);
321
+ if (!params.result.retryable) {
322
+ if (existing) {
323
+ clearDeferredQuotaRefresh(params.sessionID);
324
+ await dependencies.log("Deferred quota refresh cleared", {
325
+ sessionID: params.sessionID,
326
+ trigger: params.trigger,
327
+ reason: params.result.hasQuotaRows ? "quota_rows_available" : "not_retryable",
328
+ });
329
+ }
330
+ return;
331
+ }
332
+ if (!params.result.retryReason)
333
+ return;
334
+ scheduleDeferredQuotaRefresh({
335
+ sessionID: params.sessionID,
336
+ reason: params.result.retryReason,
337
+ incrementAttempts: params.consumedDeferredRetry,
338
+ });
339
+ }
340
+ function isTriggerEnabled(trigger) {
341
+ if (trigger === "session.idle")
342
+ return config.showOnIdle;
343
+ if (trigger === "session.compacted")
344
+ return config.showOnCompact;
345
+ if (trigger === "question")
346
+ return config.showOnQuestion;
347
+ return trigger === "deferred.retry";
348
+ }
349
+ function triggerMaintainerAnnouncementToastFallback(trigger, detectedProviderIds, runtimeConfig) {
350
+ if (!maintainerAnnouncementToastFallback.pending ||
351
+ maintainerAnnouncementToastFallback.inFlight) {
352
+ return;
353
+ }
354
+ if (!runtimeConfig.enabled || !runtimeConfig.enableToast) {
355
+ maintainerAnnouncementToastFallback.pending = false;
356
+ return;
357
+ }
358
+ if (!runtimeConfig.maintainerAnnouncements.enabled ||
359
+ !runtimeConfig.maintainerAnnouncements.home) {
360
+ maintainerAnnouncementToastFallback.pending = false;
361
+ return;
362
+ }
363
+ maintainerAnnouncementToastFallback.inFlight = true;
364
+ void (async () => {
365
+ try {
366
+ const summary = getMaintainerAnnouncementsSummary({
367
+ announcements: BUNDLED_MAINTAINER_ANNOUNCEMENTS,
368
+ enabledProviders: detectedProviderIds,
369
+ });
370
+ if (summary.activeCount <= 0) {
371
+ if (summary.futureCount <= 0) {
372
+ maintainerAnnouncementToastFallback.pending = false;
373
+ }
374
+ return;
375
+ }
376
+ const tuiDiagnostics = await inspectTuiConfig({ roots: dependencies.roots() });
377
+ if (tuiDiagnostics.quotaPluginConfigured) {
378
+ maintainerAnnouncementToastFallback.pending = false;
379
+ return;
380
+ }
381
+ const message = formatMaintainerAnnouncementHomeCountLine(summary.activeCount);
382
+ if (!message)
383
+ return;
384
+ await dependencies.showToast({
385
+ message: sanitizeDisplayText(message),
386
+ variant: "info",
387
+ duration: runtimeConfig.toastDurationMs,
388
+ });
389
+ maintainerAnnouncementToastFallback.pending = false;
390
+ await dependencies.log("Displayed maintainer announcement fallback toast", { trigger });
391
+ }
392
+ catch (error) {
393
+ await dependencies.log("Failed to show maintainer announcement fallback toast", {
394
+ trigger,
395
+ error: error instanceof Error ? error.message : String(error),
396
+ });
397
+ }
398
+ finally {
399
+ maintainerAnnouncementToastFallback.inFlight = false;
400
+ }
401
+ })();
402
+ }
403
+ async function emitQuotaToastPlan(params) {
404
+ const { config: runtimeConfig, message, detectedProviderIds, freshProviderResults, } = params.plan;
405
+ let resetNotification;
406
+ if (runtimeConfig.enableToast &&
407
+ runtimeConfig.resetNotifications.enabled &&
408
+ freshProviderResults.length > 0) {
409
+ try {
410
+ const notices = await observeQuotaResetNotifications({
411
+ providers: freshProviderResults,
412
+ windows: runtimeConfig.resetNotifications.windows,
413
+ });
414
+ resetNotification = formatQuotaResetNotification(notices) ?? undefined;
415
+ }
416
+ catch (error) {
417
+ await dependencies.log("Failed to observe quota reset transitions", {
418
+ error: error instanceof Error ? error.message : String(error),
419
+ });
420
+ }
421
+ }
422
+ if (params.plan.deferredRetryResult) {
423
+ await dependencies.log("Deferred quota refresh did not produce reportable data", {
424
+ sessionID: params.sessionID,
425
+ trigger: params.trigger,
426
+ retryable: params.plan.deferredRetryResult.retryable,
427
+ retryReason: params.plan.deferredRetryResult.retryReason,
428
+ });
429
+ return;
430
+ }
431
+ if (!message) {
432
+ await dependencies.log("No quota message to display", { trigger: params.trigger });
433
+ return;
434
+ }
435
+ if (!runtimeConfig.enableToast) {
436
+ await dependencies.log("Toast disabled (enableToast=false)", { trigger: params.trigger });
437
+ return;
438
+ }
439
+ try {
440
+ await dependencies.showToast({
441
+ message: sanitizeDisplayText(message),
442
+ variant: "info",
443
+ duration: runtimeConfig.toastDurationMs,
444
+ });
445
+ triggerMaintainerAnnouncementToastFallback(params.trigger, detectedProviderIds, runtimeConfig);
446
+ await dependencies.log("Displayed quota toast", { message, trigger: params.trigger });
447
+ if (resetNotification) {
448
+ await dependencies.showToast({
449
+ title: "Quota available",
450
+ message: sanitizeDisplayText(resetNotification),
451
+ variant: "success",
452
+ duration: runtimeConfig.toastDurationMs,
453
+ });
454
+ await dependencies.log("Displayed quota reset notification", {
455
+ message: resetNotification,
456
+ trigger: params.trigger,
457
+ });
458
+ }
459
+ }
460
+ catch (error) {
461
+ await dependencies.log("Failed to show toast", {
462
+ error: error instanceof Error ? error.message : String(error),
463
+ });
464
+ }
465
+ }
466
+ async function prepareTrigger(params) {
467
+ if (!configLoaded) {
468
+ await refreshConfig();
469
+ }
470
+ if (!config.enabled) {
471
+ clearDeferredQuotaRefresh(params.sessionID);
472
+ return null;
473
+ }
474
+ if (!isTriggerEnabled(params.trigger))
475
+ return null;
476
+ const pendingDeferred = deferredQuotaRefreshes.get(params.sessionID);
477
+ const consumedDeferredRetry = params.deferredRetry === true || Boolean(pendingDeferred);
478
+ if (pendingDeferred) {
479
+ if (pendingDeferred.inFlight && !params.deferredRetry) {
480
+ await dependencies.log("Skipping duplicate deferred quota refresh", {
481
+ sessionID: params.sessionID,
482
+ trigger: params.trigger,
483
+ });
484
+ return null;
485
+ }
486
+ pendingDeferred.inFlight = true;
487
+ clearDeferredQuotaRefreshTimer(pendingDeferred);
488
+ }
489
+ let planPrepared = false;
490
+ let completed = false;
491
+ const complete = () => {
492
+ if (completed)
493
+ return;
494
+ completed = true;
495
+ const state = deferredQuotaRefreshes.get(params.sessionID);
496
+ if (state && state === pendingDeferred) {
497
+ state.inFlight = false;
498
+ }
499
+ };
500
+ try {
501
+ if (await dependencies.isSubagentSession(params.sessionID)) {
502
+ if (consumedDeferredRetry) {
503
+ clearDeferredQuotaRefresh(params.sessionID);
504
+ }
505
+ await dependencies.log("Skipping toast for subagent session", {
506
+ sessionID: params.sessionID,
507
+ trigger: params.trigger,
508
+ });
509
+ return null;
510
+ }
511
+ const sessionMeta = await dependencies.resolveSessionMeta(params.sessionID);
512
+ const bypassForLiveLocalUsage = await shouldBypassToastCacheForLiveLocalUsage({
513
+ sessionID: params.sessionID,
514
+ sessionMeta,
515
+ });
516
+ const bypassMessageCache = config.debug || consumedDeferredRetry || bypassForLiveLocalUsage;
517
+ const bypassProviderCache = consumedDeferredRetry || bypassForLiveLocalUsage;
518
+ const toastCacheKey = buildToastCacheKey({
519
+ sessionID: params.sessionID,
520
+ sessionMeta,
521
+ });
522
+ let fetchResult;
523
+ const fetchForToast = () => fetchQuotaMessageResult({
524
+ trigger: params.trigger,
525
+ sessionID: params.sessionID,
526
+ sessionMeta,
527
+ bypassProviderCache,
528
+ });
529
+ const message = bypassMessageCache
530
+ ? await (async () => {
531
+ fetchResult = await fetchForToast();
532
+ return fetchResult.message;
533
+ })()
534
+ : await (async () => {
535
+ const fetched = {};
536
+ const cachedMessage = await getOrFetchWithCacheControl(toastCacheKey, async () => {
537
+ const result = await fetchForToast();
538
+ fetched.result = result;
539
+ return {
540
+ message: result.message,
541
+ cache: Boolean(result.message && result.cacheRenderedMessage && result.hasQuotaRows),
542
+ };
543
+ }, config.minIntervalMs);
544
+ fetchResult = fetched.result;
545
+ return cachedMessage;
546
+ })();
547
+ if (fetchResult) {
548
+ detectedProviderIdsByToastCacheKey.set(toastCacheKey, [...fetchResult.detectedProviderIds]);
549
+ await reconcileDeferredQuotaRefresh({
550
+ sessionID: params.sessionID,
551
+ result: fetchResult,
552
+ consumedDeferredRetry,
553
+ trigger: params.trigger,
554
+ });
555
+ }
556
+ planPrepared = true;
557
+ return {
558
+ config,
559
+ message,
560
+ detectedProviderIds: fetchResult?.detectedProviderIds ??
561
+ detectedProviderIdsByToastCacheKey.get(toastCacheKey) ??
562
+ [],
563
+ freshProviderResults: fetchResult?.freshProviderResults ?? [],
564
+ complete,
565
+ deferredRetryResult: params.deferredRetry && fetchResult && !fetchResult.hasQuotaRows
566
+ ? {
567
+ retryable: fetchResult.retryable,
568
+ retryReason: fetchResult.retryReason,
569
+ }
570
+ : undefined,
571
+ };
572
+ }
573
+ finally {
574
+ if (!planPrepared)
575
+ complete();
576
+ }
577
+ }
578
+ async function handleTriggerInternal(params) {
579
+ const plan = await prepareTrigger(params);
580
+ if (!plan)
581
+ return;
582
+ try {
583
+ await emitQuotaToastPlan({ ...params, plan });
584
+ }
585
+ finally {
586
+ plan.complete();
587
+ }
588
+ }
589
+ async function handleTrigger(params) {
590
+ await handleTriggerInternal(params);
591
+ }
592
+ return { handleTrigger };
593
+ }
594
+ export function formatQuotaToastDebugInfo(params) {
595
+ const availability = params.availability
596
+ ? params.availability.map((item) => `${item.id}=${item.ok ? "ok" : "no"}`).join(" ")
597
+ : "unknown";
598
+ const providers = params.config.enabledProviders === "auto"
599
+ ? "(auto)"
600
+ : params.config.enabledProviders.length > 0
601
+ ? params.config.enabledProviders.join(",")
602
+ : "(none)";
603
+ const modelPart = params.currentModel ? ` model=${params.currentModel}` : "";
604
+ const paths = params.configMeta.paths.length > 0 ? params.configMeta.paths.join(" | ") : "(none)";
605
+ return [
606
+ "Quota Toast Debug (opencode-quota)",
607
+ `trigger=${params.trigger} reason=${params.reason}`,
608
+ `configSource=${params.configMeta.source} paths=${paths}`,
609
+ `enabled=${params.config.enabled} providers=${providers}${modelPart}`,
610
+ `available=${availability}`,
611
+ ].join("\n");
612
+ }
613
+ function hasRetryableProviderError(errors) {
614
+ return errors.some((error) => error.retryable === true);
615
+ }
616
+ export async function collectQuotaToastMessage(params) {
617
+ const runtimeConfig = params.runtime.config;
618
+ const quotaResult = await collectQuotaRenderData({
619
+ client: params.runtime.client,
620
+ resolveRuntimeProviderIds: params.runtime.resolveRuntimeProviderIds,
621
+ config: runtimeConfig,
622
+ configMeta: params.runtime.configMeta,
623
+ request: createQuotaRuntimeRequestContext(params.runtime),
624
+ surfaceExplicitProviderIssues: true,
625
+ formatStyle: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
626
+ bypassProviderCache: params.bypassProviderCache,
627
+ providers: params.runtime.providers,
628
+ });
629
+ const { selection, availability, active, providerResults, attemptedAny, hasExplicitProviderIssues, data, } = quotaResult;
630
+ const detectedProviderIds = active.map((provider) => provider.id);
631
+ const collectionMetadata = {
632
+ detectedProviderIds,
633
+ freshProviderResults: providerResults,
634
+ sessionTokenError: quotaResult.sessionTokenError,
635
+ shouldReconcileDetectedProviders: selection?.isAutoMode === true,
636
+ };
637
+ const currentModel = selection?.currentModel;
638
+ const errors = data?.errors ?? [];
639
+ const hasProviderQuotaRows = Boolean(data?.entries.length);
640
+ const hasQuotaRows = Boolean(hasProviderQuotaRows || data?.sessionTokens);
641
+ const hasRetryableFetchFailure = attemptedAny && hasRetryableProviderError(errors);
642
+ const retryableAvailabilityFailure = active.length === 0 && availability.some((item) => !item.ok && item.error === true);
643
+ if (active.length === 0 && !(hasExplicitProviderIssues && errors.length > 0)) {
644
+ const message = runtimeConfig.debug
645
+ ? formatQuotaToastDebugInfo({
646
+ trigger: params.trigger,
647
+ reason: "no enabled providers available",
648
+ config: runtimeConfig,
649
+ configMeta: params.runtime.configMeta,
650
+ currentModel,
651
+ availability: availability.map((item) => ({
652
+ id: item.provider.id,
653
+ ok: item.ok,
654
+ })),
655
+ })
656
+ : null;
657
+ const retryableNoProviders = selection?.isAutoMode === true || retryableAvailabilityFailure;
658
+ return {
659
+ message,
660
+ cacheRenderedMessage: false,
661
+ retryable: retryableNoProviders,
662
+ retryReason: retryableNoProviders ? "no_available_providers" : undefined,
663
+ hasQuotaRows: false,
664
+ ...collectionMetadata,
665
+ };
666
+ }
667
+ if (hasQuotaRows) {
668
+ const formatted = formatQuotaRows({
669
+ version: "1.0.0",
670
+ layout: runtimeConfig.layout,
671
+ entries: data?.entries ?? [],
672
+ errors: data?.errors ?? [],
673
+ style: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
674
+ percentDisplayMode: runtimeConfig.percentDisplayMode,
675
+ accountingDetail: runtimeConfig.accountingDetail,
676
+ resetTimeDecimals: runtimeConfig.resetTimeDecimals,
677
+ sessionTokens: data?.sessionTokens,
678
+ });
679
+ const retryableMaskedProviderFailure = !hasProviderQuotaRows && hasRetryableFetchFailure;
680
+ if (!runtimeConfig.debug) {
681
+ return {
682
+ message: formatted,
683
+ cacheRenderedMessage: true,
684
+ retryable: retryableMaskedProviderFailure,
685
+ retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
686
+ hasQuotaRows: true,
687
+ ...collectionMetadata,
688
+ };
689
+ }
690
+ const debugFooter = `\n\n[debug] src=${params.runtime.configMeta.source} providers=${runtimeConfig.enabledProviders === "auto" ? "(auto)" : runtimeConfig.enabledProviders.join(",") || "(none)"} avail=${availability
691
+ .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`)
692
+ .join(" ")}`;
693
+ return {
694
+ message: formatted + debugFooter,
695
+ cacheRenderedMessage: false,
696
+ retryable: retryableMaskedProviderFailure,
697
+ retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
698
+ hasQuotaRows: true,
699
+ ...collectionMetadata,
700
+ };
701
+ }
702
+ if ((runtimeConfig.showOnBothFail && attemptedAny && errors.length > 0) ||
703
+ hasExplicitProviderIssues) {
704
+ const errorLines = errors.map((error) => `${error.label}: ${error.message}`).join("\n");
705
+ const retryableFetchFailure = hasRetryableFetchFailure;
706
+ const retryableFailure = retryableFetchFailure || retryableAvailabilityFailure;
707
+ const retryReason = retryableFetchFailure
708
+ ? "provider_fetch_failed"
709
+ : retryableAvailabilityFailure
710
+ ? "no_available_providers"
711
+ : undefined;
712
+ const message = !runtimeConfig.debug
713
+ ? errorLines || "Quota unavailable"
714
+ : (errorLines || "Quota unavailable") +
715
+ "\n\n" +
716
+ formatQuotaToastDebugInfo({
717
+ trigger: params.trigger,
718
+ reason: hasExplicitProviderIssues
719
+ ? "providers missing/unavailable"
720
+ : "all providers failed",
721
+ config: runtimeConfig,
722
+ configMeta: params.runtime.configMeta,
723
+ currentModel,
724
+ availability: availability.map((item) => ({
725
+ id: item.provider.id,
726
+ ok: item.ok,
727
+ })),
728
+ });
729
+ return {
730
+ message,
731
+ cacheRenderedMessage: false,
732
+ retryable: retryableFailure,
733
+ retryReason,
734
+ hasQuotaRows: false,
735
+ ...collectionMetadata,
736
+ };
737
+ }
738
+ const retryableNoData = hasRetryableFetchFailure ||
739
+ (selection?.isAutoMode === true && active.length > 0 && errors.length === 0);
740
+ return {
741
+ message: runtimeConfig.debug
742
+ ? formatQuotaToastDebugInfo({
743
+ trigger: params.trigger,
744
+ reason: "no entries",
745
+ config: runtimeConfig,
746
+ configMeta: params.runtime.configMeta,
747
+ currentModel,
748
+ availability: availability.map((item) => ({
749
+ id: item.provider.id,
750
+ ok: item.ok,
751
+ })),
752
+ })
753
+ : null,
754
+ cacheRenderedMessage: false,
755
+ retryable: retryableNoData,
756
+ retryReason: hasRetryableFetchFailure
757
+ ? "provider_fetch_failed"
758
+ : retryableNoData
759
+ ? "no_reportable_data"
760
+ : undefined,
761
+ hasQuotaRows: false,
762
+ ...collectionMetadata,
763
+ };
764
+ }
765
+ //# sourceMappingURL=quota-toast-runtime.js.map