@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
@@ -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,245 +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"]) &&
152
- typeof error.label === "string" &&
153
- typeof error.message === "string");
154
- }
155
- function isQuotaProviderDiagnostic(value) {
156
- if (!value || typeof value !== "object" || Array.isArray(value))
157
- return false;
158
- const diagnostic = value;
159
- return (hasOnlyKeys(diagnostic, [
160
- "sourceId",
161
- "providerId",
162
- "mode",
163
- "format",
164
- "modelIds",
165
- "apiKeyEnv",
166
- "selected",
167
- "attempted",
168
- "credentialSource",
169
- "outcome",
170
- "httpStatus",
171
- "entryCount",
172
- "checkedPaths",
173
- "authPaths",
174
- "statePath",
175
- "stateHealth",
176
- "stateVersion",
177
- "stateLastUpdatedAt",
178
- ]) &&
179
- typeof diagnostic.sourceId === "string" &&
180
- typeof diagnostic.providerId === "string" &&
181
- ["remote-api", "local-estimate"].includes(String(diagnostic.mode)) &&
182
- (diagnostic.format === undefined ||
183
- ["quota-v1", "openrouter-key-v1", "json-v1"].includes(String(diagnostic.format))) &&
184
- (diagnostic.mode === "remote-api"
185
- ? diagnostic.format !== undefined
186
- : diagnostic.format === undefined) &&
187
- (diagnostic.modelIds === null ||
188
- (Array.isArray(diagnostic.modelIds) &&
189
- diagnostic.modelIds.every((modelId) => typeof modelId === "string"))) &&
190
- (diagnostic.apiKeyEnv === null || typeof diagnostic.apiKeyEnv === "string") &&
191
- diagnostic.selected === true &&
192
- typeof diagnostic.attempted === "boolean" &&
193
- (diagnostic.credentialSource === null ||
194
- ["explicit_env", "global_opencode_json", "global_opencode_jsonc", "auth_json"].includes(String(diagnostic.credentialSource))) &&
195
- [
196
- "missing_credential",
197
- "success",
198
- "http_error",
199
- "redirect_error",
200
- "timeout",
201
- "body_too_large",
202
- "invalid_content_type",
203
- "invalid_json",
204
- "invalid_response",
205
- "network_error",
206
- "local_state_error",
207
- ].includes(String(diagnostic.outcome)) &&
208
- (diagnostic.httpStatus === undefined ||
209
- (typeof diagnostic.httpStatus === "number" &&
210
- Number.isInteger(diagnostic.httpStatus) &&
211
- diagnostic.httpStatus >= 100 &&
212
- diagnostic.httpStatus <= 599)) &&
213
- typeof diagnostic.entryCount === "number" &&
214
- Number.isInteger(diagnostic.entryCount) &&
215
- diagnostic.entryCount >= 0 &&
216
- Array.isArray(diagnostic.checkedPaths) &&
217
- diagnostic.checkedPaths.every((path) => typeof path === "string") &&
218
- Array.isArray(diagnostic.authPaths) &&
219
- diagnostic.authPaths.every((path) => typeof path === "string") &&
220
- (diagnostic.statePath === undefined || typeof diagnostic.statePath === "string") &&
221
- (diagnostic.stateHealth === undefined ||
222
- ["missing", "healthy", "malformed", "version_mismatch"].includes(String(diagnostic.stateHealth))) &&
223
- (diagnostic.stateVersion === undefined ||
224
- diagnostic.stateVersion === null ||
225
- (typeof diagnostic.stateVersion === "number" &&
226
- Number.isInteger(diagnostic.stateVersion) &&
227
- diagnostic.stateVersion >= 0)) &&
228
- (diagnostic.stateLastUpdatedAt === undefined ||
229
- diagnostic.stateLastUpdatedAt === null ||
230
- (typeof diagnostic.stateLastUpdatedAt === "number" &&
231
- Number.isFinite(diagnostic.stateLastUpdatedAt))));
62
+ return `provider-${createHash("sha256").update(providerId).digest("hex")}`;
232
63
  }
233
- function isQuotaProviderStatusDetail(value) {
234
- if (!value || typeof value !== "object" || Array.isArray(value))
235
- return false;
236
- const detail = value;
237
- return (hasOnlyKeys(detail, ["key", "value"]) &&
238
- typeof detail.key === "string" &&
239
- typeof detail.value === "string");
240
- }
241
- function isQuotaProviderPresentation(value) {
242
- if (!value || typeof value !== "object" || Array.isArray(value))
243
- return false;
244
- const presentation = value;
245
- return (hasOnlyKeys(presentation, [
246
- "singleWindowDisplayName",
247
- "singleWindowShowRight",
248
- "redundantQuotaFamily",
249
- "classicStrategy",
250
- ]) &&
251
- (presentation.singleWindowDisplayName === undefined ||
252
- typeof presentation.singleWindowDisplayName === "string") &&
253
- (presentation.singleWindowShowRight === undefined ||
254
- typeof presentation.singleWindowShowRight === "boolean") &&
255
- (presentation.redundantQuotaFamily === undefined ||
256
- typeof presentation.redundantQuotaFamily === "string") &&
257
- (presentation.classicStrategy === undefined || presentation.classicStrategy === "preserve"));
258
- }
259
- function isQuotaProviderResult(value) {
260
- if (!value || typeof value !== "object" || Array.isArray(value))
261
- return false;
262
- const result = value;
263
- return (hasOnlyKeys(result, [
264
- "attempted",
265
- "entries",
266
- "errors",
267
- "diagnostics",
268
- "statusDetails",
269
- "rawDetails",
270
- "presentation",
271
- ]) &&
272
- typeof result.attempted === "boolean" &&
273
- Array.isArray(result.entries) &&
274
- result.entries.every(isQuotaToastEntry) &&
275
- Array.isArray(result.errors) &&
276
- result.errors.every(isQuotaToastError) &&
277
- (result.diagnostics === undefined ||
278
- (Array.isArray(result.diagnostics) && result.diagnostics.every(isQuotaProviderDiagnostic))) &&
279
- (result.statusDetails === undefined ||
280
- (Array.isArray(result.statusDetails) &&
281
- result.statusDetails.every(isQuotaProviderStatusDetail))) &&
282
- (result.rawDetails === undefined ||
283
- (Array.isArray(result.rawDetails) && result.rawDetails.every(isQuotaProviderStatusDetail))) &&
284
- (result.presentation === undefined || isQuotaProviderPresentation(result.presentation)));
64
+ export function getQuotaProviderStateCacheFilePath(providerId, key) {
65
+ return join(getQuotaProviderCacheDir(), `${getQuotaProviderCacheFileStem(providerId)}-${getQuotaProviderStateCacheLocator(key)}.json`);
285
66
  }
286
67
  async function getQuotaProviderCachePackageVersion() {
287
68
  return (await getPackageVersion()) ?? QUOTA_PROVIDER_CACHE_PACKAGE_VERSION_FALLBACK;
288
69
  }
289
- function isPersistedQuotaProviderCacheEntry(value, key, providerId, packageVersion) {
290
- if (!value || typeof value !== "object") {
291
- return false;
292
- }
293
- const entry = value;
294
- return (entry.version === QUOTA_PROVIDER_CACHE_VERSION &&
295
- entry.packageVersion === packageVersion &&
296
- entry.key === key &&
297
- entry.providerId === providerId &&
298
- typeof entry.timestamp === "number" &&
299
- Number.isFinite(entry.timestamp) &&
300
- isQuotaProviderResult(entry.result));
301
- }
302
70
  async function safeRm(path) {
303
71
  try {
304
72
  await rm(path, { force: true, recursive: true });
@@ -340,32 +108,31 @@ async function readPersistedQuotaProviderCacheEntry(params) {
340
108
  return null;
341
109
  }
342
110
  const path = getQuotaProviderStateCacheFilePath(params.providerId, params.key);
111
+ const cacheLocator = getQuotaProviderStateCacheLocator(params.key);
343
112
  try {
344
113
  const raw = await readFile(path, "utf-8");
345
114
  const parsed = JSON.parse(raw);
346
- 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) {
347
121
  await safeRm(path);
348
122
  return null;
349
123
  }
350
- if (!params.ignoreExpiry && params.now - parsed.timestamp >= params.ttlMs) {
124
+ if (!params.ignoreExpiry && params.now - decoded.timestamp >= params.ttlMs) {
351
125
  return null;
352
126
  }
353
- return {
354
- version: parsed.version,
355
- packageVersion: parsed.packageVersion,
356
- key: parsed.key,
357
- providerId: parsed.providerId,
358
- timestamp: parsed.timestamp,
359
- result: cloneQuotaProviderResult(parsed.result),
360
- };
127
+ return decoded;
361
128
  }
362
129
  catch {
363
130
  return null;
364
131
  }
365
132
  }
366
- async function writePersistedQuotaProviderCacheEntry(entry) {
133
+ async function writePersistedQuotaProviderCacheEntry(entry, logicalKey) {
367
134
  try {
368
- await writeJsonAtomic(getQuotaProviderStateCacheFilePath(entry.providerId, entry.key), entry, {
135
+ await writeJsonAtomic(getQuotaProviderStateCacheFilePath(entry.providerId, logicalKey), entry, {
369
136
  trailingNewline: true,
370
137
  });
371
138
  }
@@ -373,11 +140,11 @@ async function writePersistedQuotaProviderCacheEntry(entry) {
373
140
  // persistence failures should not break quota fetches
374
141
  }
375
142
  }
376
- async function fetchValidatedProviderResult(provider, ctx) {
377
- const fetched = await provider.fetch(ctx);
378
- if (isQuotaProviderResult(fetched)) {
379
- return cloneQuotaProviderResult(fetched);
380
- }
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;
381
148
  return {
382
149
  attempted: true,
383
150
  entries: [],
@@ -394,8 +161,7 @@ async function resolveRuntimeEligibleQuotaProviders(providerId, ctx) {
394
161
  return undefined;
395
162
  }
396
163
  try {
397
- const response = await ctx.client.config.providers();
398
- const availableProviderIds = new Set((response.data?.providers ?? []).map((provider) => provider.id));
164
+ const availableProviderIds = await ctx.resolveRuntimeProviderIds();
399
165
  return selectEligibleQuotaProviderDefinitions({
400
166
  definitions: ctx.config.quotaProviders ?? [],
401
167
  availableProviderIds,
@@ -408,16 +174,77 @@ async function resolveRuntimeEligibleQuotaProviders(providerId, ctx) {
408
174
  return null;
409
175
  }
410
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
+ }
411
238
  function publishQuotaTelemetry(params) {
412
239
  if (!params.ctx.config.telemetryToken)
413
240
  return;
414
241
  const uncachedSnapshotId = `uncached:${params.providerId}`;
242
+ const cachedSnapshotId = `cached:${params.providerId}`;
243
+ const isUncached = params.snapshotId.startsWith("uncached:");
415
244
  updateQuotaTelemetrySnapshot({
416
245
  token: params.ctx.config.telemetryToken,
417
- snapshotId: params.snapshotId,
418
- ...(params.snapshotId !== uncachedSnapshotId
419
- ? { supersededSnapshotIds: [uncachedSnapshotId] }
420
- : {}),
246
+ snapshotId: isUncached ? uncachedSnapshotId : cachedSnapshotId,
247
+ supersededSnapshotIds: [isUncached ? cachedSnapshotId : uncachedSnapshotId],
421
248
  providerId: params.providerId,
422
249
  result: params.result,
423
250
  ...(params.cacheTimestamp !== undefined ? { cacheTimestamp: params.cacheTimestamp } : {}),
@@ -425,8 +252,9 @@ function publishQuotaTelemetry(params) {
425
252
  }
426
253
  export async function fetchQuotaProviderResult(params) {
427
254
  const { provider, ctx, ttlMs, bypassCache = false } = params;
428
- if (bypassCache) {
429
- 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 });
430
258
  publishQuotaTelemetry({
431
259
  ctx,
432
260
  providerId: provider.id,
@@ -434,34 +262,48 @@ export async function fetchQuotaProviderResult(params) {
434
262
  result: snapshot,
435
263
  });
436
264
  return snapshot;
437
- }
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
+ });
438
279
  if (isLiveLocalUsageProviderId(provider.id)) {
439
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
440
- publishQuotaTelemetry({
441
- 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),
442
287
  providerId: provider.id,
443
- snapshotId: `uncached:${provider.id}`,
288
+ timestamp: Date.now(),
444
289
  result: snapshot,
445
290
  });
446
- return snapshot;
447
- }
448
- const runtimeEligibleQuotaProviders = await resolveRuntimeEligibleQuotaProviders(provider.id, ctx);
449
- if (runtimeEligibleQuotaProviders === null) {
450
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
291
+ inMemoryCache.set(key, {
292
+ ...entry,
293
+ result: cloneQuotaProviderResult(entry.result),
294
+ });
451
295
  publishQuotaTelemetry({
452
296
  ctx,
453
297
  providerId: provider.id,
454
- snapshotId: `uncached:${provider.id}`,
298
+ snapshotId: key,
455
299
  result: snapshot,
300
+ cacheTimestamp: entry.timestamp,
456
301
  });
457
302
  return snapshot;
458
303
  }
459
304
  const forceAggregateRefresh = provider.id === QUOTA_PROVIDERS_AGGREGATE_ID &&
460
305
  runtimeEligibleQuotaProviders?.some((definition) => definition.mode === "local-estimate") ===
461
306
  true;
462
- const key = buildQuotaProviderStateCacheKey(provider.id, ctx, {
463
- runtimeEligibleQuotaProviders,
464
- });
465
307
  const now = Date.now();
466
308
  const packageVersion = await getQuotaProviderCachePackageVersion();
467
309
  await maybePrunePersistedQuotaProviderCache(now);
@@ -514,26 +356,39 @@ export async function fetchQuotaProviderResult(params) {
514
356
  });
515
357
  return cloneQuotaProviderResult(persisted.result);
516
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
+ }
517
371
  const fetchPromise = (async () => {
518
- const snapshot = await fetchValidatedProviderResult(provider, ctx);
519
- 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))) {
520
376
  inMemoryCache.delete(key);
521
377
  await safeRm(getQuotaProviderStateCacheFilePath(provider.id, key));
522
378
  return snapshot;
523
379
  }
524
- const entry = {
525
- version: QUOTA_PROVIDER_CACHE_VERSION,
380
+ const entry = encodePersistedQuotaProviderCacheEntry({
526
381
  packageVersion,
527
- key,
382
+ key: getQuotaProviderStateCacheLocator(key),
528
383
  providerId: provider.id,
529
384
  timestamp: Date.now(),
530
- result: cloneQuotaProviderResult(snapshot),
531
- };
385
+ result: snapshot,
386
+ });
532
387
  inMemoryCache.set(key, {
533
388
  ...entry,
534
389
  result: cloneQuotaProviderResult(entry.result),
535
390
  });
536
- await writePersistedQuotaProviderCacheEntry(entry);
391
+ await writePersistedQuotaProviderCacheEntry(entry, key);
537
392
  return snapshot;
538
393
  })().finally(() => {
539
394
  inFlightByKey.delete(key);
@@ -551,14 +406,21 @@ export async function fetchQuotaProviderResult(params) {
551
406
  }
552
407
  export async function readCachedProviderResult(params) {
553
408
  const runtimeEligibleQuotaProviders = await resolveRuntimeEligibleQuotaProviders(params.provider.id, params.ctx);
554
- if (runtimeEligibleQuotaProviders === null) {
409
+ if (runtimeEligibleQuotaProviders === null)
555
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
+ });
556
418
  }
557
419
  const key = buildQuotaProviderStateCacheKey(params.provider.id, params.ctx, {
558
420
  runtimeEligibleQuotaProviders,
421
+ resolvedAuthIdentity: scope.resolvedAuthIdentity,
559
422
  });
560
423
  const now = Date.now();
561
- // Check in-memory cache first.
562
424
  const inMemory = inMemoryCache.get(key);
563
425
  if (inMemory) {
564
426
  publishQuotaTelemetry({
@@ -574,7 +436,6 @@ export async function readCachedProviderResult(params) {
574
436
  timestamp: inMemory.timestamp,
575
437
  };
576
438
  }
577
- // Fall back to disk cache with no expiry guard.
578
439
  const packageVersion = await getQuotaProviderCachePackageVersion();
579
440
  const persisted = await readPersistedQuotaProviderCacheEntry({
580
441
  key,
@@ -585,7 +446,6 @@ export async function readCachedProviderResult(params) {
585
446
  ignoreExpiry: true,
586
447
  });
587
448
  if (persisted) {
588
- // Populate in-memory cache for subsequent reads.
589
449
  inMemoryCache.set(key, {
590
450
  ...persisted,
591
451
  result: cloneQuotaProviderResult(persisted.result),
@@ -608,6 +468,7 @@ export async function readCachedProviderResult(params) {
608
468
  export function __resetQuotaStateForTests() {
609
469
  inMemoryCache.clear();
610
470
  inFlightByKey.clear();
471
+ processLocalLatestByRuntime = new WeakMap();
611
472
  lastPruneAtMs = 0;
612
473
  }
613
474
  //# sourceMappingURL=quota-state.js.map