@slkiser/opencode-quota 4.8.2 → 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 (227) 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 +75 -22
  30. package/dist/lib/display-sanitize.js.map +1 -1
  31. package/dist/lib/entries.d.ts +105 -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/google-agy.d.ts +2 -0
  40. package/dist/lib/google-agy.d.ts.map +1 -1
  41. package/dist/lib/google-agy.js +29 -14
  42. package/dist/lib/google-agy.js.map +1 -1
  43. package/dist/lib/google-gemini-cli.d.ts +2 -0
  44. package/dist/lib/google-gemini-cli.d.ts.map +1 -1
  45. package/dist/lib/google-gemini-cli.js +29 -3
  46. package/dist/lib/google-gemini-cli.js.map +1 -1
  47. package/dist/lib/google-token-cache.d.ts +5 -8
  48. package/dist/lib/google-token-cache.d.ts.map +1 -1
  49. package/dist/lib/google-token-cache.js +30 -118
  50. package/dist/lib/google-token-cache.js.map +1 -1
  51. package/dist/lib/google.d.ts +2 -0
  52. package/dist/lib/google.d.ts.map +1 -1
  53. package/dist/lib/google.js +31 -6
  54. package/dist/lib/google.js.map +1 -1
  55. package/dist/lib/grouped-entry-normalization.d.ts.map +1 -1
  56. package/dist/lib/grouped-entry-normalization.js +53 -20
  57. package/dist/lib/grouped-entry-normalization.js.map +1 -1
  58. package/dist/lib/minimax-auth.d.ts +1 -1
  59. package/dist/lib/minimax-auth.js +1 -1
  60. package/dist/lib/nanogpt.d.ts +5 -5
  61. package/dist/lib/nanogpt.d.ts.map +1 -1
  62. package/dist/lib/nanogpt.js +59 -23
  63. package/dist/lib/nanogpt.js.map +1 -1
  64. package/dist/lib/openai.d.ts +4 -0
  65. package/dist/lib/openai.d.ts.map +1 -1
  66. package/dist/lib/openai.js +22 -3
  67. package/dist/lib/openai.js.map +1 -1
  68. package/dist/lib/opencode-go-auth.js +1 -1
  69. package/dist/lib/opencode-go-auth.js.map +1 -1
  70. package/dist/lib/opencode-go.d.ts.map +1 -1
  71. package/dist/lib/opencode-go.js +6 -1
  72. package/dist/lib/opencode-go.js.map +1 -1
  73. package/dist/lib/opencode-zen-config.d.ts +0 -1
  74. package/dist/lib/opencode-zen-config.d.ts.map +1 -1
  75. package/dist/lib/opencode-zen-config.js +0 -22
  76. package/dist/lib/opencode-zen-config.js.map +1 -1
  77. package/dist/lib/opencode-zen.d.ts +3 -0
  78. package/dist/lib/opencode-zen.d.ts.map +1 -1
  79. package/dist/lib/opencode-zen.js +18 -1
  80. package/dist/lib/opencode-zen.js.map +1 -1
  81. package/dist/lib/openrouter.d.ts +2 -0
  82. package/dist/lib/openrouter.d.ts.map +1 -1
  83. package/dist/lib/openrouter.js +10 -0
  84. package/dist/lib/openrouter.js.map +1 -1
  85. package/dist/lib/provider-metadata.d.ts +2 -307
  86. package/dist/lib/provider-metadata.d.ts.map +1 -1
  87. package/dist/lib/provider-metadata.js +14 -316
  88. package/dist/lib/provider-metadata.js.map +1 -1
  89. package/dist/lib/provider-registration.d.ts +325 -0
  90. package/dist/lib/provider-registration.d.ts.map +1 -0
  91. package/dist/lib/provider-registration.js +338 -0
  92. package/dist/lib/provider-registration.js.map +1 -0
  93. package/dist/lib/quota-accounting-projection.d.ts +7 -0
  94. package/dist/lib/quota-accounting-projection.d.ts.map +1 -0
  95. package/dist/lib/quota-accounting-projection.js +239 -0
  96. package/dist/lib/quota-accounting-projection.js.map +1 -0
  97. package/dist/lib/quota-command-format.d.ts +3 -2
  98. package/dist/lib/quota-command-format.d.ts.map +1 -1
  99. package/dist/lib/quota-command-format.js +35 -9
  100. package/dist/lib/quota-command-format.js.map +1 -1
  101. package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
  102. package/dist/lib/quota-dialog-commands.js +1 -0
  103. package/dist/lib/quota-dialog-commands.js.map +1 -1
  104. package/dist/lib/quota-export.d.ts +2 -1
  105. package/dist/lib/quota-export.d.ts.map +1 -1
  106. package/dist/lib/quota-export.js +36 -10
  107. package/dist/lib/quota-export.js.map +1 -1
  108. package/dist/lib/quota-render-data.d.ts +0 -1
  109. package/dist/lib/quota-render-data.d.ts.map +1 -1
  110. package/dist/lib/quota-render-data.js +9 -138
  111. package/dist/lib/quota-render-data.js.map +1 -1
  112. package/dist/lib/quota-reset-notifications.d.ts.map +1 -1
  113. package/dist/lib/quota-reset-notifications.js +3 -0
  114. package/dist/lib/quota-reset-notifications.js.map +1 -1
  115. package/dist/lib/quota-state-codec.d.ts +16 -0
  116. package/dist/lib/quota-state-codec.d.ts.map +1 -0
  117. package/dist/lib/quota-state-codec.js +409 -0
  118. package/dist/lib/quota-state-codec.js.map +1 -0
  119. package/dist/lib/quota-state.d.ts +2 -11
  120. package/dist/lib/quota-state.d.ts.map +1 -1
  121. package/dist/lib/quota-state.js +165 -305
  122. package/dist/lib/quota-state.js.map +1 -1
  123. package/dist/lib/quota-status.d.ts.map +1 -1
  124. package/dist/lib/quota-status.js +14 -7
  125. package/dist/lib/quota-status.js.map +1 -1
  126. package/dist/lib/quota-telemetry.d.ts.map +1 -1
  127. package/dist/lib/quota-telemetry.js +7 -3
  128. package/dist/lib/quota-telemetry.js.map +1 -1
  129. package/dist/lib/quota-toast-runtime.d.ts +60 -0
  130. package/dist/lib/quota-toast-runtime.d.ts.map +1 -0
  131. package/dist/lib/quota-toast-runtime.js +765 -0
  132. package/dist/lib/quota-toast-runtime.js.map +1 -0
  133. package/dist/lib/resolved-auth-identity.d.ts +36 -0
  134. package/dist/lib/resolved-auth-identity.d.ts.map +1 -0
  135. package/dist/lib/resolved-auth-identity.js +193 -0
  136. package/dist/lib/resolved-auth-identity.js.map +1 -0
  137. package/dist/lib/scoped-update-migration.d.ts +101 -0
  138. package/dist/lib/scoped-update-migration.d.ts.map +1 -0
  139. package/dist/lib/scoped-update-migration.js +348 -0
  140. package/dist/lib/scoped-update-migration.js.map +1 -0
  141. package/dist/lib/scoped-update.d.ts +8 -0
  142. package/dist/lib/scoped-update.d.ts.map +1 -1
  143. package/dist/lib/scoped-update.js +263 -50
  144. package/dist/lib/scoped-update.js.map +1 -1
  145. package/dist/lib/toast-format-grouped.d.ts +1 -0
  146. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  147. package/dist/lib/toast-format-grouped.js +111 -24
  148. package/dist/lib/toast-format-grouped.js.map +1 -1
  149. package/dist/lib/tui-compact-format.d.ts +1 -0
  150. package/dist/lib/tui-compact-format.d.ts.map +1 -1
  151. package/dist/lib/tui-compact-format.js +133 -22
  152. package/dist/lib/tui-compact-format.js.map +1 -1
  153. package/dist/lib/tui-panel-state.d.ts +2 -0
  154. package/dist/lib/tui-panel-state.d.ts.map +1 -1
  155. package/dist/lib/tui-panel-state.js.map +1 -1
  156. package/dist/lib/tui-runtime.d.ts.map +1 -1
  157. package/dist/lib/tui-runtime.js +84 -10
  158. package/dist/lib/tui-runtime.js.map +1 -1
  159. package/dist/lib/tui-sidebar-format.d.ts +1 -1
  160. package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
  161. package/dist/lib/tui-sidebar-format.js +1 -0
  162. package/dist/lib/tui-sidebar-format.js.map +1 -1
  163. package/dist/lib/types.d.ts +7 -1
  164. package/dist/lib/types.d.ts.map +1 -1
  165. package/dist/lib/types.js +1 -0
  166. package/dist/lib/types.js.map +1 -1
  167. package/dist/lib/xai.d.ts +2 -0
  168. package/dist/lib/xai.d.ts.map +1 -1
  169. package/dist/lib/xai.js +10 -0
  170. package/dist/lib/xai.js.map +1 -1
  171. package/dist/plugin.d.ts.map +1 -1
  172. package/dist/plugin.js +27 -720
  173. package/dist/plugin.js.map +1 -1
  174. package/dist/providers/accounting-decimal.d.ts +2 -0
  175. package/dist/providers/accounting-decimal.d.ts.map +1 -0
  176. package/dist/providers/accounting-decimal.js +23 -0
  177. package/dist/providers/accounting-decimal.js.map +1 -0
  178. package/dist/providers/cache-policies.d.ts +64 -0
  179. package/dist/providers/cache-policies.d.ts.map +1 -0
  180. package/dist/providers/cache-policies.js +197 -0
  181. package/dist/providers/cache-policies.js.map +1 -0
  182. package/dist/providers/cursor.d.ts.map +1 -1
  183. package/dist/providers/cursor.js +73 -58
  184. package/dist/providers/cursor.js.map +1 -1
  185. package/dist/providers/deepseek.d.ts +2 -2
  186. package/dist/providers/deepseek.d.ts.map +1 -1
  187. package/dist/providers/deepseek.js +64 -33
  188. package/dist/providers/deepseek.js.map +1 -1
  189. package/dist/providers/google-agy.js +2 -2
  190. package/dist/providers/google-agy.js.map +1 -1
  191. package/dist/providers/kilo.d.ts.map +1 -1
  192. package/dist/providers/kilo.js +56 -29
  193. package/dist/providers/kilo.js.map +1 -1
  194. package/dist/providers/mimo.d.ts.map +1 -1
  195. package/dist/providers/mimo.js +37 -15
  196. package/dist/providers/mimo.js.map +1 -1
  197. package/dist/providers/minimax-coding-plan.d.ts +8 -7
  198. package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
  199. package/dist/providers/minimax-coding-plan.js +97 -83
  200. package/dist/providers/minimax-coding-plan.js.map +1 -1
  201. package/dist/providers/nanogpt.d.ts.map +1 -1
  202. package/dist/providers/nanogpt.js +72 -51
  203. package/dist/providers/nanogpt.js.map +1 -1
  204. package/dist/providers/openai.d.ts.map +1 -1
  205. package/dist/providers/openai.js +0 -3
  206. package/dist/providers/openai.js.map +1 -1
  207. package/dist/providers/opencode-go.d.ts.map +1 -1
  208. package/dist/providers/opencode-go.js +3 -4
  209. package/dist/providers/opencode-go.js.map +1 -1
  210. package/dist/providers/opencode-zen.d.ts.map +1 -1
  211. package/dist/providers/opencode-zen.js +78 -17
  212. package/dist/providers/opencode-zen.js.map +1 -1
  213. package/dist/providers/quota-providers.d.ts.map +1 -1
  214. package/dist/providers/quota-providers.js +31 -23
  215. package/dist/providers/quota-providers.js.map +1 -1
  216. package/dist/providers/registry.d.ts +3 -2
  217. package/dist/providers/registry.d.ts.map +1 -1
  218. package/dist/providers/registry.js +41 -31
  219. package/dist/providers/registry.js.map +1 -1
  220. package/dist/providers/result-helpers.d.ts +10 -5
  221. package/dist/providers/result-helpers.d.ts.map +1 -1
  222. package/dist/providers/result-helpers.js +3 -3
  223. package/dist/providers/result-helpers.js.map +1 -1
  224. package/dist/tui.d.ts.map +1 -1
  225. package/dist/tui.js +11 -3
  226. package/dist/tui.tsx +16 -7
  227. package/package.json +1 -1
@@ -1,47 +1,21 @@
1
- import { createHash } from "crypto";
2
- import { readdir, readFile, rm, stat } from "fs/promises";
3
- import { join } from "path";
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, rm, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
4
  import { writeJsonAtomic } from "./atomic-json.js";
5
5
  import { getOpencodeRuntimeDirs } from "./opencode-runtime-paths.js";
6
6
  import { getQuotaProviderDisplayLabel, isLiveLocalUsageProviderId } from "./provider-metadata.js";
7
7
  import { QUOTA_PROVIDERS_AGGREGATE_ID, selectEligibleQuotaProviderDefinitions, } from "./quota-providers.js";
8
+ import { cloneQuotaProviderResult, decodePersistedQuotaProviderCacheEntry, encodePersistedQuotaProviderCacheEntry, normalizeQuotaProviderResult, } from "./quota-state-codec.js";
8
9
  import { updateQuotaTelemetrySnapshot } from "./quota-telemetry.js";
9
10
  import { getPackageVersion } from "./version.js";
10
- const QUOTA_PROVIDER_CACHE_VERSION = 2;
11
11
  const QUOTA_PROVIDER_CACHE_PACKAGE_VERSION_FALLBACK = "unknown";
12
12
  const QUOTA_PROVIDER_CACHE_DIRNAME = "quota-provider-state";
13
13
  const QUOTA_PROVIDER_CACHE_RETENTION_MS = 24 * 60 * 60 * 1000;
14
14
  const QUOTA_PROVIDER_CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
15
15
  const inMemoryCache = new Map();
16
16
  const inFlightByKey = new Map();
17
+ let processLocalLatestByRuntime = new WeakMap();
17
18
  let lastPruneAtMs = 0;
18
- export function cloneQuotaProviderResult(result) {
19
- return {
20
- attempted: result.attempted,
21
- entries: result.entries.map((entry) => ({
22
- ...entry,
23
- accounting: { ...entry.accounting },
24
- })),
25
- errors: result.errors.map((error) => ({ ...error })),
26
- ...(result.diagnostics
27
- ? {
28
- diagnostics: result.diagnostics.map((diagnostic) => ({
29
- ...diagnostic,
30
- modelIds: diagnostic.modelIds ? [...diagnostic.modelIds] : null,
31
- checkedPaths: [...diagnostic.checkedPaths],
32
- authPaths: [...diagnostic.authPaths],
33
- })),
34
- }
35
- : {}),
36
- ...(result.statusDetails
37
- ? { statusDetails: result.statusDetails.map((detail) => ({ ...detail })) }
38
- : {}),
39
- ...(result.rawDetails
40
- ? { rawDetails: result.rawDetails.map((detail) => ({ ...detail })) }
41
- : {}),
42
- ...(result.presentation ? { presentation: { ...result.presentation } } : {}),
43
- };
44
- }
45
19
  export function buildQuotaProviderStateCacheKey(providerId, ctx, options = {}) {
46
20
  const googleModels = ctx.config.googleModels.join(",");
47
21
  const cursorPlan = ctx.config.cursorPlan;
@@ -60,246 +34,39 @@ export function buildQuotaProviderStateCacheKey(providerId, ctx, options = {}) {
60
34
  const quotaProvidersIdentity = relevantQuotaProviders.length > 0
61
35
  ? `|quotaProviders=${JSON.stringify(["quota-providers-cache-v1", relevantQuotaProviders])}`
62
36
  : "";
37
+ const resolvedAuthIdentity = options.resolvedAuthIdentity
38
+ ? `|resolvedAuthIdentity=${options.resolvedAuthIdentity}`
39
+ : "";
63
40
  const runtimeEligibleIdentity = isAggregateCache
64
41
  ? `|runtimeEligibleQuotaProviders=${JSON.stringify([
65
42
  "quota-providers-runtime-eligible-v1",
66
43
  options.runtimeEligibleQuotaProviders ?? [],
67
44
  ])}`
68
45
  : "";
69
- return `${providerId}${quotaProvidersIdentity}${runtimeEligibleIdentity}|anthropicBinaryPath=${anthropicBinaryPath}|googleModels=${googleModels}|cursorPlan=${cursorPlan}|cursorIncludedApiUsd=${cursorIncludedApiUsd}|cursorBillingCycleStartDay=${cursorBillingCycleStartDay}|opencodeGoWindows=${opencodeGoWindows}|onlyCurrentModel=${onlyCurrentModel}|currentModel=${currentModel}|currentProviderID=${currentProviderID}`;
46
+ return `${providerId}${quotaProvidersIdentity}${runtimeEligibleIdentity}|anthropicBinaryPath=${anthropicBinaryPath}|googleModels=${googleModels}|cursorPlan=${cursorPlan}|cursorIncludedApiUsd=${cursorIncludedApiUsd}|cursorBillingCycleStartDay=${cursorBillingCycleStartDay}|opencodeGoWindows=${opencodeGoWindows}|onlyCurrentModel=${onlyCurrentModel}|currentModel=${currentModel}|currentProviderID=${currentProviderID}${resolvedAuthIdentity}`;
70
47
  }
71
48
  function getQuotaProviderCacheDir() {
72
49
  return join(getOpencodeRuntimeDirs().cacheDir, QUOTA_PROVIDER_CACHE_DIRNAME);
73
50
  }
74
- export function getQuotaProviderStateCacheFilePath(providerId, key) {
75
- const digest = createHash("sha1").update(key).digest("hex");
76
- return join(getQuotaProviderCacheDir(), `${providerId}-${digest}.json`);
77
- }
78
- function hasOnlyKeys(value, allowed) {
79
- const allowedKeys = new Set(allowed);
80
- return Object.keys(value).every((key) => allowedKeys.has(key));
81
- }
82
- const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
83
- function isOptionalIsoTimestamp(value) {
84
- return (value === undefined ||
85
- (typeof value === "string" &&
86
- ISO_TIMESTAMP_RE.test(value) &&
87
- Number.isFinite(Date.parse(value))));
51
+ function getQuotaProviderStateCacheLocator(key) {
52
+ return createHash("sha256").update(key).digest("hex");
88
53
  }
89
- function isAccountingMetadata(value) {
90
- if (!value || typeof value !== "object" || Array.isArray(value))
91
- return false;
92
- const accounting = value;
93
- return (hasOnlyKeys(accounting, [
94
- "resultType",
95
- "acquisitionMethod",
96
- "ownership",
97
- "authority",
98
- "sourceId",
99
- "observedAtIso",
100
- ]) &&
101
- ["quota", "rate_limit", "usage", "spend", "budget", "balance", "status"].includes(String(accounting.resultType)) &&
102
- [
103
- "remote_api",
104
- "dashboard_scrape",
105
- "local_cli",
106
- "local_runtime_accounting",
107
- "local_estimation",
108
- ].includes(String(accounting.acquisitionMethod)) &&
109
- ["maintained", "user_configured"].includes(String(accounting.ownership)) &&
110
- ["provider_reported", "locally_derived"].includes(String(accounting.authority)) &&
111
- (accounting.sourceId === undefined || typeof accounting.sourceId === "string") &&
112
- isOptionalIsoTimestamp(accounting.observedAtIso));
113
- }
114
- function isQuotaToastEntry(value) {
115
- if (!value || typeof value !== "object" || Array.isArray(value))
116
- return false;
117
- const entry = value;
118
- if (!hasOnlyKeys(entry, [
119
- "accounting",
120
- "kind",
121
- "name",
122
- "percentRemaining",
123
- "value",
124
- "resetTimeIso",
125
- "group",
126
- "label",
127
- "metricLabel",
128
- "right",
129
- "sortPriority",
130
- ]) ||
131
- !isAccountingMetadata(entry.accounting) ||
132
- typeof entry.name !== "string" ||
133
- !isOptionalIsoTimestamp(entry.resetTimeIso) ||
134
- (entry.sortPriority !== undefined &&
135
- (typeof entry.sortPriority !== "number" || !Number.isFinite(entry.sortPriority))) ||
136
- !["group", "label", "metricLabel", "right"].every((key) => entry[key] === undefined || typeof entry[key] === "string")) {
137
- return false;
138
- }
139
- if (entry.kind === "value") {
140
- return typeof entry.value === "string" && entry.percentRemaining === undefined;
54
+ function getQuotaProviderCacheFileStem(providerId) {
55
+ const windowsReserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu;
56
+ if (providerId !== "." &&
57
+ providerId !== ".." &&
58
+ /^[A-Za-z0-9._-]+$/u.test(providerId) &&
59
+ !windowsReserved.test(providerId)) {
60
+ return providerId;
141
61
  }
142
- return ((entry.kind === undefined || entry.kind === "percent") &&
143
- typeof entry.percentRemaining === "number" &&
144
- Number.isFinite(entry.percentRemaining) &&
145
- entry.value === undefined);
146
- }
147
- function isQuotaToastError(value) {
148
- if (!value || typeof value !== "object" || Array.isArray(value))
149
- return false;
150
- const error = value;
151
- return (hasOnlyKeys(error, ["label", "message", "kind"]) &&
152
- typeof error.label === "string" &&
153
- typeof error.message === "string" &&
154
- (error.kind === undefined || error.kind === "intentional-filter"));
155
- }
156
- function isQuotaProviderDiagnostic(value) {
157
- if (!value || typeof value !== "object" || Array.isArray(value))
158
- return false;
159
- const diagnostic = value;
160
- return (hasOnlyKeys(diagnostic, [
161
- "sourceId",
162
- "providerId",
163
- "mode",
164
- "format",
165
- "modelIds",
166
- "apiKeyEnv",
167
- "selected",
168
- "attempted",
169
- "credentialSource",
170
- "outcome",
171
- "httpStatus",
172
- "entryCount",
173
- "checkedPaths",
174
- "authPaths",
175
- "statePath",
176
- "stateHealth",
177
- "stateVersion",
178
- "stateLastUpdatedAt",
179
- ]) &&
180
- typeof diagnostic.sourceId === "string" &&
181
- typeof diagnostic.providerId === "string" &&
182
- ["remote-api", "local-estimate"].includes(String(diagnostic.mode)) &&
183
- (diagnostic.format === undefined ||
184
- ["quota-v1", "openrouter-key-v1", "json-v1"].includes(String(diagnostic.format))) &&
185
- (diagnostic.mode === "remote-api"
186
- ? diagnostic.format !== undefined
187
- : diagnostic.format === undefined) &&
188
- (diagnostic.modelIds === null ||
189
- (Array.isArray(diagnostic.modelIds) &&
190
- diagnostic.modelIds.every((modelId) => typeof modelId === "string"))) &&
191
- (diagnostic.apiKeyEnv === null || typeof diagnostic.apiKeyEnv === "string") &&
192
- diagnostic.selected === true &&
193
- typeof diagnostic.attempted === "boolean" &&
194
- (diagnostic.credentialSource === null ||
195
- ["explicit_env", "global_opencode_json", "global_opencode_jsonc", "auth_json"].includes(String(diagnostic.credentialSource))) &&
196
- [
197
- "missing_credential",
198
- "success",
199
- "http_error",
200
- "redirect_error",
201
- "timeout",
202
- "body_too_large",
203
- "invalid_content_type",
204
- "invalid_json",
205
- "invalid_response",
206
- "network_error",
207
- "local_state_error",
208
- ].includes(String(diagnostic.outcome)) &&
209
- (diagnostic.httpStatus === undefined ||
210
- (typeof diagnostic.httpStatus === "number" &&
211
- Number.isInteger(diagnostic.httpStatus) &&
212
- diagnostic.httpStatus >= 100 &&
213
- diagnostic.httpStatus <= 599)) &&
214
- typeof diagnostic.entryCount === "number" &&
215
- Number.isInteger(diagnostic.entryCount) &&
216
- diagnostic.entryCount >= 0 &&
217
- Array.isArray(diagnostic.checkedPaths) &&
218
- diagnostic.checkedPaths.every((path) => typeof path === "string") &&
219
- Array.isArray(diagnostic.authPaths) &&
220
- diagnostic.authPaths.every((path) => typeof path === "string") &&
221
- (diagnostic.statePath === undefined || typeof diagnostic.statePath === "string") &&
222
- (diagnostic.stateHealth === undefined ||
223
- ["missing", "healthy", "malformed", "version_mismatch"].includes(String(diagnostic.stateHealth))) &&
224
- (diagnostic.stateVersion === undefined ||
225
- diagnostic.stateVersion === null ||
226
- (typeof diagnostic.stateVersion === "number" &&
227
- Number.isInteger(diagnostic.stateVersion) &&
228
- diagnostic.stateVersion >= 0)) &&
229
- (diagnostic.stateLastUpdatedAt === undefined ||
230
- diagnostic.stateLastUpdatedAt === null ||
231
- (typeof diagnostic.stateLastUpdatedAt === "number" &&
232
- Number.isFinite(diagnostic.stateLastUpdatedAt))));
62
+ return `provider-${createHash("sha256").update(providerId).digest("hex")}`;
233
63
  }
234
- function isQuotaProviderStatusDetail(value) {
235
- if (!value || typeof value !== "object" || Array.isArray(value))
236
- return false;
237
- const detail = value;
238
- return (hasOnlyKeys(detail, ["key", "value"]) &&
239
- typeof detail.key === "string" &&
240
- typeof detail.value === "string");
241
- }
242
- function isQuotaProviderPresentation(value) {
243
- if (!value || typeof value !== "object" || Array.isArray(value))
244
- return false;
245
- const presentation = value;
246
- return (hasOnlyKeys(presentation, [
247
- "singleWindowDisplayName",
248
- "singleWindowShowRight",
249
- "redundantQuotaFamily",
250
- "classicStrategy",
251
- ]) &&
252
- (presentation.singleWindowDisplayName === undefined ||
253
- typeof presentation.singleWindowDisplayName === "string") &&
254
- (presentation.singleWindowShowRight === undefined ||
255
- typeof presentation.singleWindowShowRight === "boolean") &&
256
- (presentation.redundantQuotaFamily === undefined ||
257
- typeof presentation.redundantQuotaFamily === "string") &&
258
- (presentation.classicStrategy === undefined || presentation.classicStrategy === "preserve"));
259
- }
260
- function isQuotaProviderResult(value) {
261
- if (!value || typeof value !== "object" || Array.isArray(value))
262
- return false;
263
- const result = value;
264
- return (hasOnlyKeys(result, [
265
- "attempted",
266
- "entries",
267
- "errors",
268
- "diagnostics",
269
- "statusDetails",
270
- "rawDetails",
271
- "presentation",
272
- ]) &&
273
- typeof result.attempted === "boolean" &&
274
- Array.isArray(result.entries) &&
275
- result.entries.every(isQuotaToastEntry) &&
276
- Array.isArray(result.errors) &&
277
- result.errors.every(isQuotaToastError) &&
278
- (result.diagnostics === undefined ||
279
- (Array.isArray(result.diagnostics) && result.diagnostics.every(isQuotaProviderDiagnostic))) &&
280
- (result.statusDetails === undefined ||
281
- (Array.isArray(result.statusDetails) &&
282
- result.statusDetails.every(isQuotaProviderStatusDetail))) &&
283
- (result.rawDetails === undefined ||
284
- (Array.isArray(result.rawDetails) && result.rawDetails.every(isQuotaProviderStatusDetail))) &&
285
- (result.presentation === undefined || isQuotaProviderPresentation(result.presentation)));
64
+ export function getQuotaProviderStateCacheFilePath(providerId, key) {
65
+ return join(getQuotaProviderCacheDir(), `${getQuotaProviderCacheFileStem(providerId)}-${getQuotaProviderStateCacheLocator(key)}.json`);
286
66
  }
287
67
  async function getQuotaProviderCachePackageVersion() {
288
68
  return (await getPackageVersion()) ?? QUOTA_PROVIDER_CACHE_PACKAGE_VERSION_FALLBACK;
289
69
  }
290
- function isPersistedQuotaProviderCacheEntry(value, key, providerId, packageVersion) {
291
- if (!value || typeof value !== "object") {
292
- return false;
293
- }
294
- const entry = value;
295
- return (entry.version === QUOTA_PROVIDER_CACHE_VERSION &&
296
- entry.packageVersion === packageVersion &&
297
- entry.key === key &&
298
- entry.providerId === providerId &&
299
- typeof entry.timestamp === "number" &&
300
- Number.isFinite(entry.timestamp) &&
301
- isQuotaProviderResult(entry.result));
302
- }
303
70
  async function safeRm(path) {
304
71
  try {
305
72
  await rm(path, { force: true, recursive: true });
@@ -341,32 +108,31 @@ async function readPersistedQuotaProviderCacheEntry(params) {
341
108
  return null;
342
109
  }
343
110
  const path = getQuotaProviderStateCacheFilePath(params.providerId, params.key);
111
+ const cacheLocator = getQuotaProviderStateCacheLocator(params.key);
344
112
  try {
345
113
  const raw = await readFile(path, "utf-8");
346
114
  const parsed = JSON.parse(raw);
347
- if (!isPersistedQuotaProviderCacheEntry(parsed, params.key, params.providerId, params.packageVersion)) {
115
+ const decoded = decodePersistedQuotaProviderCacheEntry(parsed, {
116
+ key: cacheLocator,
117
+ providerId: params.providerId,
118
+ packageVersion: params.packageVersion,
119
+ });
120
+ if (!decoded) {
348
121
  await safeRm(path);
349
122
  return null;
350
123
  }
351
- if (!params.ignoreExpiry && params.now - parsed.timestamp >= params.ttlMs) {
124
+ if (!params.ignoreExpiry && params.now - decoded.timestamp >= params.ttlMs) {
352
125
  return null;
353
126
  }
354
- return {
355
- version: parsed.version,
356
- packageVersion: parsed.packageVersion,
357
- key: parsed.key,
358
- providerId: parsed.providerId,
359
- timestamp: parsed.timestamp,
360
- result: cloneQuotaProviderResult(parsed.result),
361
- };
127
+ return decoded;
362
128
  }
363
129
  catch {
364
130
  return null;
365
131
  }
366
132
  }
367
- async function writePersistedQuotaProviderCacheEntry(entry) {
133
+ async function writePersistedQuotaProviderCacheEntry(entry, logicalKey) {
368
134
  try {
369
- await writeJsonAtomic(getQuotaProviderStateCacheFilePath(entry.providerId, entry.key), entry, {
135
+ await writeJsonAtomic(getQuotaProviderStateCacheFilePath(entry.providerId, logicalKey), entry, {
370
136
  trailingNewline: true,
371
137
  });
372
138
  }
@@ -374,11 +140,11 @@ async function writePersistedQuotaProviderCacheEntry(entry) {
374
140
  // persistence failures should not break quota fetches
375
141
  }
376
142
  }
377
- async function fetchValidatedProviderResult(provider, ctx) {
378
- const fetched = await provider.fetch(ctx);
379
- if (isQuotaProviderResult(fetched)) {
380
- return cloneQuotaProviderResult(fetched);
381
- }
143
+ async function fetchValidatedProviderResult(provider, ctx, cacheContext) {
144
+ const fetched = await provider.fetch(ctx, cacheContext);
145
+ const normalized = normalizeQuotaProviderResult(fetched);
146
+ if (normalized)
147
+ return normalized;
382
148
  return {
383
149
  attempted: true,
384
150
  entries: [],
@@ -395,8 +161,7 @@ async function resolveRuntimeEligibleQuotaProviders(providerId, ctx) {
395
161
  return undefined;
396
162
  }
397
163
  try {
398
- const response = await ctx.client.config.providers();
399
- const availableProviderIds = new Set((response.data?.providers ?? []).map((provider) => provider.id));
164
+ const availableProviderIds = await ctx.resolveRuntimeProviderIds();
400
165
  return selectEligibleQuotaProviderDefinitions({
401
166
  definitions: ctx.config.quotaProviders ?? [],
402
167
  availableProviderIds,
@@ -409,16 +174,77 @@ async function resolveRuntimeEligibleQuotaProviders(providerId, ctx) {
409
174
  return null;
410
175
  }
411
176
  }
177
+ async function resolveProviderCacheScope(provider, ctx, cacheContext) {
178
+ const policy = provider.cachePolicy;
179
+ if (!policy || policy.kind === "uncached")
180
+ return null;
181
+ if (policy.kind === "account-neutral")
182
+ return {};
183
+ try {
184
+ const resolvedAuthIdentity = await policy.resolveIdentity(ctx, cacheContext);
185
+ return resolvedAuthIdentity ? { resolvedAuthIdentity } : null;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ async function isProviderCacheScopeCurrent(provider, ctx, scope) {
192
+ if (provider.cachePolicy?.kind === "account-neutral")
193
+ return true;
194
+ if (provider.cachePolicy?.kind !== "resolved-auth")
195
+ return false;
196
+ try {
197
+ const runtimeEligibleQuotaProviders = await resolveRuntimeEligibleQuotaProviders(provider.id, ctx);
198
+ if (runtimeEligibleQuotaProviders === null)
199
+ return false;
200
+ return ((await provider.cachePolicy.resolveIdentity(ctx, { runtimeEligibleQuotaProviders })) ===
201
+ scope.resolvedAuthIdentity);
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }
207
+ function getProcessLocalRuntimeOwner(ctx) {
208
+ const owner = ctx.client;
209
+ return owner && (typeof owner === "object" || typeof owner === "function") ? owner : null;
210
+ }
211
+ function rememberProcessLocalLatest(params) {
212
+ const runtimeOwner = getProcessLocalRuntimeOwner(params.ctx);
213
+ if (!runtimeOwner)
214
+ return;
215
+ let runtimeSnapshots = processLocalLatestByRuntime.get(runtimeOwner);
216
+ if (!runtimeSnapshots) {
217
+ runtimeSnapshots = new WeakMap();
218
+ processLocalLatestByRuntime.set(runtimeOwner, runtimeSnapshots);
219
+ }
220
+ runtimeSnapshots.set(params.provider, {
221
+ result: cloneQuotaProviderResult(params.result),
222
+ timestamp: Date.now(),
223
+ });
224
+ }
225
+ function readProcessLocalLatest(params) {
226
+ const runtimeOwner = getProcessLocalRuntimeOwner(params.ctx);
227
+ if (!runtimeOwner)
228
+ return { hit: false };
229
+ const entry = processLocalLatestByRuntime.get(runtimeOwner)?.get(params.provider);
230
+ if (!entry)
231
+ return { hit: false };
232
+ return {
233
+ hit: true,
234
+ result: cloneQuotaProviderResult(entry.result),
235
+ timestamp: entry.timestamp,
236
+ };
237
+ }
412
238
  function publishQuotaTelemetry(params) {
413
239
  if (!params.ctx.config.telemetryToken)
414
240
  return;
415
241
  const uncachedSnapshotId = `uncached:${params.providerId}`;
242
+ const cachedSnapshotId = `cached:${params.providerId}`;
243
+ const isUncached = params.snapshotId.startsWith("uncached:");
416
244
  updateQuotaTelemetrySnapshot({
417
245
  token: params.ctx.config.telemetryToken,
418
- snapshotId: params.snapshotId,
419
- ...(params.snapshotId !== uncachedSnapshotId
420
- ? { supersededSnapshotIds: [uncachedSnapshotId] }
421
- : {}),
246
+ snapshotId: isUncached ? uncachedSnapshotId : cachedSnapshotId,
247
+ supersededSnapshotIds: [isUncached ? cachedSnapshotId : uncachedSnapshotId],
422
248
  providerId: params.providerId,
423
249
  result: params.result,
424
250
  ...(params.cacheTimestamp !== undefined ? { cacheTimestamp: params.cacheTimestamp } : {}),
@@ -426,8 +252,9 @@ function publishQuotaTelemetry(params) {
426
252
  }
427
253
  export async function fetchQuotaProviderResult(params) {
428
254
  const { provider, ctx, ttlMs, bypassCache = false } = params;
429
- if (bypassCache) {
430
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
255
+ const fetchUncached = async (cacheContext = {}) => {
256
+ const snapshot = await fetchValidatedProviderResult(provider, ctx, cacheContext);
257
+ rememberProcessLocalLatest({ provider, ctx, result: snapshot });
431
258
  publishQuotaTelemetry({
432
259
  ctx,
433
260
  providerId: provider.id,
@@ -435,34 +262,48 @@ export async function fetchQuotaProviderResult(params) {
435
262
  result: snapshot,
436
263
  });
437
264
  return snapshot;
438
- }
265
+ };
266
+ if (bypassCache && !isLiveLocalUsageProviderId(provider.id))
267
+ return fetchUncached();
268
+ const runtimeEligibleQuotaProviders = await resolveRuntimeEligibleQuotaProviders(provider.id, ctx);
269
+ if (runtimeEligibleQuotaProviders === null)
270
+ return fetchUncached();
271
+ const cacheContext = { runtimeEligibleQuotaProviders };
272
+ const scope = await resolveProviderCacheScope(provider, ctx, cacheContext);
273
+ if (!scope)
274
+ return fetchUncached(cacheContext);
275
+ const key = buildQuotaProviderStateCacheKey(provider.id, ctx, {
276
+ runtimeEligibleQuotaProviders,
277
+ resolvedAuthIdentity: scope.resolvedAuthIdentity,
278
+ });
439
279
  if (isLiveLocalUsageProviderId(provider.id)) {
440
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
441
- publishQuotaTelemetry({
442
- ctx,
280
+ const snapshot = await fetchValidatedProviderResult(provider, ctx, cacheContext);
281
+ if (!(await isProviderCacheScopeCurrent(provider, ctx, scope))) {
282
+ return snapshot;
283
+ }
284
+ const entry = encodePersistedQuotaProviderCacheEntry({
285
+ packageVersion: await getQuotaProviderCachePackageVersion(),
286
+ key: getQuotaProviderStateCacheLocator(key),
443
287
  providerId: provider.id,
444
- snapshotId: `uncached:${provider.id}`,
288
+ timestamp: Date.now(),
445
289
  result: snapshot,
446
290
  });
447
- return snapshot;
448
- }
449
- const runtimeEligibleQuotaProviders = await resolveRuntimeEligibleQuotaProviders(provider.id, ctx);
450
- if (runtimeEligibleQuotaProviders === null) {
451
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
291
+ inMemoryCache.set(key, {
292
+ ...entry,
293
+ result: cloneQuotaProviderResult(entry.result),
294
+ });
452
295
  publishQuotaTelemetry({
453
296
  ctx,
454
297
  providerId: provider.id,
455
- snapshotId: `uncached:${provider.id}`,
298
+ snapshotId: key,
456
299
  result: snapshot,
300
+ cacheTimestamp: entry.timestamp,
457
301
  });
458
302
  return snapshot;
459
303
  }
460
304
  const forceAggregateRefresh = provider.id === QUOTA_PROVIDERS_AGGREGATE_ID &&
461
305
  runtimeEligibleQuotaProviders?.some((definition) => definition.mode === "local-estimate") ===
462
306
  true;
463
- const key = buildQuotaProviderStateCacheKey(provider.id, ctx, {
464
- runtimeEligibleQuotaProviders,
465
- });
466
307
  const now = Date.now();
467
308
  const packageVersion = await getQuotaProviderCachePackageVersion();
468
309
  await maybePrunePersistedQuotaProviderCache(now);
@@ -515,26 +356,39 @@ export async function fetchQuotaProviderResult(params) {
515
356
  });
516
357
  return cloneQuotaProviderResult(persisted.result);
517
358
  }
359
+ const inFlightAfterDiskRead = inFlightByKey.get(key);
360
+ if (inFlightAfterDiskRead) {
361
+ const snapshot = await inFlightAfterDiskRead;
362
+ publishQuotaTelemetry({
363
+ ctx,
364
+ providerId: provider.id,
365
+ snapshotId: key,
366
+ result: snapshot,
367
+ cacheTimestamp: inMemoryCache.get(key)?.timestamp,
368
+ });
369
+ return cloneQuotaProviderResult(snapshot);
370
+ }
518
371
  const fetchPromise = (async () => {
519
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
520
- if (!snapshot.attempted || snapshot.entries.length === 0) {
372
+ const snapshot = await fetchValidatedProviderResult(provider, ctx, cacheContext);
373
+ if (!snapshot.attempted ||
374
+ snapshot.entries.length === 0 ||
375
+ !(await isProviderCacheScopeCurrent(provider, ctx, scope))) {
521
376
  inMemoryCache.delete(key);
522
377
  await safeRm(getQuotaProviderStateCacheFilePath(provider.id, key));
523
378
  return snapshot;
524
379
  }
525
- const entry = {
526
- version: QUOTA_PROVIDER_CACHE_VERSION,
380
+ const entry = encodePersistedQuotaProviderCacheEntry({
527
381
  packageVersion,
528
- key,
382
+ key: getQuotaProviderStateCacheLocator(key),
529
383
  providerId: provider.id,
530
384
  timestamp: Date.now(),
531
- result: cloneQuotaProviderResult(snapshot),
532
- };
385
+ result: snapshot,
386
+ });
533
387
  inMemoryCache.set(key, {
534
388
  ...entry,
535
389
  result: cloneQuotaProviderResult(entry.result),
536
390
  });
537
- await writePersistedQuotaProviderCacheEntry(entry);
391
+ await writePersistedQuotaProviderCacheEntry(entry, key);
538
392
  return snapshot;
539
393
  })().finally(() => {
540
394
  inFlightByKey.delete(key);
@@ -552,14 +406,21 @@ export async function fetchQuotaProviderResult(params) {
552
406
  }
553
407
  export async function readCachedProviderResult(params) {
554
408
  const runtimeEligibleQuotaProviders = await resolveRuntimeEligibleQuotaProviders(params.provider.id, params.ctx);
555
- if (runtimeEligibleQuotaProviders === null) {
409
+ if (runtimeEligibleQuotaProviders === null)
556
410
  return { hit: false };
411
+ const cacheContext = { runtimeEligibleQuotaProviders };
412
+ const scope = await resolveProviderCacheScope(params.provider, params.ctx, cacheContext);
413
+ if (!scope) {
414
+ return readProcessLocalLatest({
415
+ provider: params.provider,
416
+ ctx: params.ctx,
417
+ });
557
418
  }
558
419
  const key = buildQuotaProviderStateCacheKey(params.provider.id, params.ctx, {
559
420
  runtimeEligibleQuotaProviders,
421
+ resolvedAuthIdentity: scope.resolvedAuthIdentity,
560
422
  });
561
423
  const now = Date.now();
562
- // Check in-memory cache first.
563
424
  const inMemory = inMemoryCache.get(key);
564
425
  if (inMemory) {
565
426
  publishQuotaTelemetry({
@@ -575,7 +436,6 @@ export async function readCachedProviderResult(params) {
575
436
  timestamp: inMemory.timestamp,
576
437
  };
577
438
  }
578
- // Fall back to disk cache with no expiry guard.
579
439
  const packageVersion = await getQuotaProviderCachePackageVersion();
580
440
  const persisted = await readPersistedQuotaProviderCacheEntry({
581
441
  key,
@@ -586,7 +446,6 @@ export async function readCachedProviderResult(params) {
586
446
  ignoreExpiry: true,
587
447
  });
588
448
  if (persisted) {
589
- // Populate in-memory cache for subsequent reads.
590
449
  inMemoryCache.set(key, {
591
450
  ...persisted,
592
451
  result: cloneQuotaProviderResult(persisted.result),
@@ -609,6 +468,7 @@ export async function readCachedProviderResult(params) {
609
468
  export function __resetQuotaStateForTests() {
610
469
  inMemoryCache.clear();
611
470
  inFlightByKey.clear();
471
+ processLocalLatestByRuntime = new WeakMap();
612
472
  lastPruneAtMs = 0;
613
473
  }
614
474
  //# sourceMappingURL=quota-state.js.map