authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-29 15:28:50-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-29 15:28:50-05:00
log1a553d0f5dd2aa39a64895ec770dc51ef1448b1a
tree630ef60d6e04c7d408965c12d0338c3a673aeccd
parent451afcb22d93f06cb24f9bc16703404564952553
signature Signed by PGP key B5690EEEBB952194

fix(shared): merge OpenCode Go limits by credential (#14209)


5 files changed, 130 insertions(+), 32 deletions(-)

apps/server/src/provider/Layers/OpenCodeProvider.test.ts+6
......@@ -1,4 +1,5 @@
11import * as NodeAssert from "node:assert/strict";
2import * as NodeCrypto from "node:crypto";
23
34import * as NodeServices from "@effect/platform-node/NodeServices";
45import { it } from "@effect/vitest";
......@@ -71,6 +72,10 @@ it.effect("reads Go limits with the instance's XDG credentials and preserves res
7172 Effect.provide(NodeServices.layer),
7273 );
7374 NodeAssert.equal(limits.unavailable, undefined);
75 NodeAssert.equal(
76 limits.credentialFingerprint,
77 NodeCrypto.createHash("sha256").update("opencode-go\0instance-key").digest("hex"),
78 );
7479 NodeAssert.deepEqual(
7580 limits.windows.map(({ kind, usedPercent, resetsAt: reset }) => ({
7681 kind,
......@@ -139,6 +144,7 @@ it.effect("keeps Go entitlement absence distinct from failed or malformed usage
139144 Effect.provide(NodeServices.layer),
140145 );
141146 NodeAssert.equal(limits.unavailable?.reason, reason);
147 NodeAssert.equal(limits.credentialFingerprint, undefined);
142148 NodeAssert.deepEqual(limits.windows, []);
143149 }
144150 }),
apps/server/src/provider/Layers/openCodeUsageLimits.ts+11-1
......@@ -1,4 +1,5 @@
11import * as NodeOS from "node:os";
2import * as NodeCrypto from "node:crypto";
23
34import type { ServerProviderUsageWindow } from "@t3tools/contracts";
45import * as DateTime from "effect/DateTime";
......@@ -95,7 +96,16 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
9596 resetsAt: DateTime.formatIso(body.usage.monthly.resetsAt),
9697 },
9798 ];
98 return makeUsageLimits({ checkedAt, windows });
99 return {
100 ...makeUsageLimits({ checkedAt, windows }),
101 // Go's usage response has no account ID. An unkeyed hash matches across
102 // environments without a shared secret. It permits offline guesses, but
103 // Go keys are randomly generated.
104 credentialFingerprint: NodeCrypto.createHash("sha256")
105 .update("opencode-go\0")
106 .update(apiKey)
107 .digest("hex"),
108 };
99109 }).pipe(
100110 Effect.timeout("5 seconds"),
101111 Effect.orElseSucceed(() =>
packages/contracts/src/providerUsageLimits.ts+2
......@@ -50,6 +50,8 @@ export type ServerProviderResetCredits = typeof ServerProviderResetCredits.Type;
5050export const ServerProviderUsageLimits = Schema.Struct({
5151 checkedAt: IsoDateTime,
5252 windows: ForwardCompatibleArray(ServerProviderUsageWindow),
53 /** Opaque credential identity when the provider does not report an account. */
54 credentialFingerprint: Schema.optional(TrimmedNonEmptyString),
5355 resetCredits: Schema.optional(ServerProviderResetCredits),
5456 /** Provider-owned usage settings when quota windows are not available to the client. */
5557 externalUsage: Schema.optional(
packages/shared/src/usageLimits.test.ts+68
......@@ -207,6 +207,74 @@ describe("pools", () => {
207207 expect(accounts[0]?.limits.windows[0]?.usedPercent).toBe(55);
208208 });
209209
210 it("merges OpenCode Go limits from machines with the same API key", () => {
211 const go = provider({
212 driver: ProviderDriverKind.make("opencode"),
213 instanceId: ProviderInstanceId.make("opencode"),
214 auth: { status: "authenticated" },
215 usageLimits: {
216 checkedAt,
217 credentialFingerprint: "shared-go-key",
218 windows: [{ ...window, id: "go_rolling", usedPercent: 3 }],
219 },
220 });
221 const input = new Map([
222 [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { providers: [go] } }],
223 [
224 EnvironmentId.make("env-b"),
225 {
226 entry: { target: { label: "Desktop" } },
227 serverConfig: {
228 providers: [
229 {
230 ...go,
231 usageLimits: {
232 ...go.usageLimits!,
233 checkedAt: "2026-09-03T11:30:00.000Z",
234 windows: [{ ...window, id: "go_rolling", usedPercent: 4 }],
235 },
236 },
237 ],
238 },
239 },
240 ],
241 ]);
242 const accounts = collectLimitAccounts(input);
243 expect(accounts).toHaveLength(1);
244 expect(accounts[0]?.environments).toEqual([
245 { environmentId: "env-a", label: "Laptop" },
246 { environmentId: "env-b", label: "Desktop" },
247 ]);
248 expect(collectLimitPools(accounts, now)[0]?.windows[0]?.members).toHaveLength(1);
249 expect(accounts[0]?.limits.windows[0]?.usedPercent).toBe(4);
250
251 const differentKey = {
252 ...go,
253 usageLimits: { ...go.usageLimits!, credentialFingerprint: "other-go-key" },
254 };
255 input.set(EnvironmentId.make("env-b"), {
256 entry: { target: { label: "Desktop" } },
257 serverConfig: { providers: [differentKey] },
258 });
259 expect(collectLimitAccounts(input)).toHaveLength(2);
260
261 input.set(EnvironmentId.make("env-a"), {
262 ...laptop,
263 serverConfig: {
264 providers: [{ ...go, auth: { status: "authenticated", email: "same@example.com" } }],
265 },
266 });
267 input.set(EnvironmentId.make("env-b"), {
268 entry: { target: { label: "Desktop" } },
269 serverConfig: {
270 providers: [
271 { ...differentKey, auth: { status: "authenticated", email: "SAME@example.com" } },
272 ],
273 },
274 });
275 expect(collectLimitAccounts(input)).toHaveLength(1);
276 });
277
210278 it("takes windows from a fresher hub read but credits and redeem from the native instance", () => {
211279 const native = provider({
212280 driver: claude,
packages/shared/src/usageLimits.ts+43-31
......@@ -136,16 +136,24 @@ export function collectExternalUsageLinks(presentations: LimitPresentations) {
136136 return [...links.values()];
137137}
138138
139function accountKey(driver: ServerProvider["driver"], email: string | undefined): string | null {
139/** Prefer the reported email; use an identical credential when no email is available. */
140function accountKey(
141 driver: ServerProvider["driver"],
142 email: string | undefined,
143 limits?: ServerProviderUsageLimits,
144): string | null {
140145 const normalizedEmail = email?.trim().toLowerCase();
141 return normalizedEmail ? `${driver}:${normalizedEmail}` : null;
146 if (normalizedEmail) return `${driver}:${normalizedEmail}`;
147 return limits?.credentialFingerprint
148 ? `${driver}:credential:${limits.credentialFingerprint}`
149 : null;
142150}
143151
144152/**
145153 * One subscription account as the pooled views see it, whichever way it was
146 * reported. The same email signed in natively on two environments, or reported
147 * by a hub as well as natively, is one account: its quota is one bucket, so
148 * counting it twice would misstate what is left.
154 * reported. Matching emails or credentials across environments name
155 * one account. Its quota is one bucket, so counting it twice would misstate
156 * what is left.
149157 */
150158export interface LimitAccount {
151159 readonly key: string;
......@@ -243,7 +251,7 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl
243251 for (const provider of providersWithLimits(presentation.serverConfig?.providers ?? [])) {
244252 if (!provider.usageLimits || limitsNotice(provider.usageLimits) !== null) continue;
245253 merge(
246 accountKey(provider.driver, provider.auth.email) ??
254 accountKey(provider.driver, provider.auth.email, provider.usageLimits) ??
247255 `${environmentId}:${provider.instanceId}`,
248256 {
249257 key: `${environmentId}:${provider.instanceId}`,
......@@ -271,27 +279,31 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl
271279 : source.label;
272280 for (const account of source.accounts) {
273281 if (limitsNotice(account.usageLimits) !== null) continue;
274 merge(accountKey(account.driver, account.email) ?? `${source.id}:${account.id}`, {
275 key: `${source.id}:${account.id}`,
276 driver: account.driver,
277 displayName: account.email ? null : account.id.replace(/\.json$/i, ""),
278 email: account.email,
279 plan: account.plan,
280 accentColor: undefined,
281 environments: [],
282 sourceLabel,
283 redeem: account.usageLimits.resetCredits?.nextCreditId
284 ? {
285 environmentId,
286 input: {
287 sourceId: source.id,
288 accountId: account.id,
289 creditId: account.usageLimits.resetCredits.nextCreditId,
290 },
291 }
292 : null,
293 limits: account.usageLimits,
294 });
282 merge(
283 accountKey(account.driver, account.email, account.usageLimits) ??
284 `${source.id}:${account.id}`,
285 {
286 key: `${source.id}:${account.id}`,
287 driver: account.driver,
288 displayName: account.email ? null : account.id.replace(/\.json$/i, ""),
289 email: account.email,
290 plan: account.plan,
291 accentColor: undefined,
292 environments: [],
293 sourceLabel,
294 redeem: account.usageLimits.resetCredits?.nextCreditId
295 ? {
296 environmentId,
297 input: {
298 sourceId: source.id,
299 accountId: account.id,
300 creditId: account.usageLimits.resetCredits.nextCreditId,
301 },
302 }
303 : null,
304 limits: account.usageLimits,
305 },
306 );
295307 }
296308 }
297309 }
......@@ -647,7 +659,7 @@ export function collectProviderUsageLimits(
647659 );
648660 const nativeAccounts = new Set(
649661 native.flatMap((provider) => {
650 const key = accountKey(provider.driver, provider.auth.email);
662 const key = accountKey(provider.driver, provider.auth.email, provider.usageLimits);
651663 return key && provider.usageLimits?.windows.length && !provider.usageLimits.unavailable
652664 ? [key]
653665 : [];
......@@ -657,13 +669,13 @@ export function collectProviderUsageLimits(
657669 const notices: string[] = [];
658670 for (const provider of native) {
659671 if (!provider.usageLimits) continue;
660 const key = accountKey(provider.driver, provider.auth.email);
672 const key = accountKey(provider.driver, provider.auth.email, provider.usageLimits);
661673 const hubCredits = sources
662674 .flatMap((source) => source.accounts.map((account) => ({ source, account })))
663675 .filter(
664676 ({ account }) =>
665677 key !== null &&
666 accountKey(account.driver, account.email) === key &&
678 accountKey(account.driver, account.email, account.usageLimits) === key &&
667679 account.usageLimits.resetCredits &&
668680 !limitsNotice(account.usageLimits),
669681 )
......@@ -710,7 +722,7 @@ export function collectProviderUsageLimits(
710722 for (const source of sources) {
711723 const matching = source.accounts.filter((account) => account.driver === selected.driver);
712724 for (const account of matching) {
713 const key = accountKey(account.driver, account.email);
725 const key = accountKey(account.driver, account.email, account.usageLimits);
714726 if (key && nativeAccounts.has(key)) continue;
715727 accounts.push({
716728 id: `${source.id}:${account.id}`,