From d5318fb50fb49a5233234b225e97045fedd691fd Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Mon, 28 Sep 2026 22:51:05 -0500 Subject: [PATCH 1/2] fix(shared): merge OpenCode Go limits by credential - Fingerprint Go API keys to identify matching accounts across environments - Keep email-based account matching as the preferred identity --- .../provider/Layers/OpenCodeProvider.test.ts | 6 ++ .../provider/Layers/openCodeUsageLimits.ts | 11 ++- packages/contracts/src/providerUsageLimits.ts | 2 + packages/shared/src/usageLimits.test.ts | 68 +++++++++++++++++ packages/shared/src/usageLimits.ts | 73 +++++++++++-------- 5 files changed, 128 insertions(+), 32 deletions(-) diff --git a/apps/server/src/provider/Layers/OpenCodeProvider.test.ts b/apps/server/src/provider/Layers/OpenCodeProvider.test.ts index b50a51f6cb31..55e159197e6b 100644 --- a/apps/server/src/provider/Layers/OpenCodeProvider.test.ts +++ b/apps/server/src/provider/Layers/OpenCodeProvider.test.ts @@ -1,4 +1,5 @@ import * as NodeAssert from "node:assert/strict"; +import * as NodeCrypto from "node:crypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; @@ -71,6 +72,10 @@ it.effect("reads Go limits with the instance's XDG credentials and preserves res Effect.provide(NodeServices.layer), ); NodeAssert.equal(limits.unavailable, undefined); + NodeAssert.equal( + limits.credentialFingerprint, + NodeCrypto.createHash("sha256").update("opencode-go\0instance-key").digest("hex"), + ); NodeAssert.deepEqual( limits.windows.map(({ kind, usedPercent, resetsAt: reset }) => ({ kind, @@ -139,6 +144,7 @@ it.effect("keeps Go entitlement absence distinct from failed or malformed usage Effect.provide(NodeServices.layer), ); NodeAssert.equal(limits.unavailable?.reason, reason); + NodeAssert.equal(limits.credentialFingerprint, undefined); NodeAssert.deepEqual(limits.windows, []); } }), diff --git a/apps/server/src/provider/Layers/openCodeUsageLimits.ts b/apps/server/src/provider/Layers/openCodeUsageLimits.ts index 22b542142cc4..4bbefd19551c 100644 --- a/apps/server/src/provider/Layers/openCodeUsageLimits.ts +++ b/apps/server/src/provider/Layers/openCodeUsageLimits.ts @@ -1,4 +1,5 @@ import * as NodeOS from "node:os"; +import * as NodeCrypto from "node:crypto"; import type { ServerProviderUsageWindow } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; @@ -95,7 +96,15 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( resetsAt: DateTime.formatIso(body.usage.monthly.resetsAt), }, ]; - return makeUsageLimits({ checkedAt, windows }); + return { + ...makeUsageLimits({ checkedAt, windows }), + // Go's usage response has no account ID, so identical keys are the only + // cross-environment identity we can establish from this probe. + credentialFingerprint: NodeCrypto.createHash("sha256") + .update("opencode-go\0") + .update(apiKey) + .digest("hex"), + }; }).pipe( Effect.timeout("5 seconds"), Effect.orElseSucceed(() => diff --git a/packages/contracts/src/providerUsageLimits.ts b/packages/contracts/src/providerUsageLimits.ts index 0f126f291aec..2dcb2ed9b3d6 100644 --- a/packages/contracts/src/providerUsageLimits.ts +++ b/packages/contracts/src/providerUsageLimits.ts @@ -50,6 +50,8 @@ export type ServerProviderResetCredits = typeof ServerProviderResetCredits.Type; export const ServerProviderUsageLimits = Schema.Struct({ checkedAt: IsoDateTime, windows: ForwardCompatibleArray(ServerProviderUsageWindow), + /** Opaque credential identity when the provider does not report an account. */ + credentialFingerprint: Schema.optional(TrimmedNonEmptyString), resetCredits: Schema.optional(ServerProviderResetCredits), unavailable: Schema.optional( Schema.Struct({ diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index 0646953a5f08..a6043116631f 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -202,6 +202,74 @@ describe("pools", () => { expect(accounts[0]?.limits.windows[0]?.usedPercent).toBe(55); }); + it("merges OpenCode Go limits from machines with the same API key", () => { + const go = provider({ + driver: ProviderDriverKind.make("opencode"), + instanceId: ProviderInstanceId.make("opencode"), + auth: { status: "authenticated" }, + usageLimits: { + checkedAt, + credentialFingerprint: "shared-go-key", + windows: [{ ...window, id: "go_rolling", usedPercent: 3 }], + }, + }); + const input = new Map([ + [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { providers: [go] } }], + [ + EnvironmentId.make("env-b"), + { + entry: { target: { label: "Desktop" } }, + serverConfig: { + providers: [ + { + ...go, + usageLimits: { + ...go.usageLimits!, + checkedAt: "2026-09-03T11:30:00.000Z", + windows: [{ ...window, id: "go_rolling", usedPercent: 4 }], + }, + }, + ], + }, + }, + ], + ]); + const accounts = collectLimitAccounts(input); + expect(accounts).toHaveLength(1); + expect(accounts[0]?.environments).toEqual([ + { environmentId: "env-a", label: "Laptop" }, + { environmentId: "env-b", label: "Desktop" }, + ]); + expect(collectLimitPools(accounts, now)[0]?.windows[0]?.members).toHaveLength(1); + expect(accounts[0]?.limits.windows[0]?.usedPercent).toBe(4); + + const differentKey = { + ...go, + usageLimits: { ...go.usageLimits!, credentialFingerprint: "other-go-key" }, + }; + input.set(EnvironmentId.make("env-b"), { + entry: { target: { label: "Desktop" } }, + serverConfig: { providers: [differentKey] }, + }); + expect(collectLimitAccounts(input)).toHaveLength(2); + + input.set(EnvironmentId.make("env-a"), { + ...laptop, + serverConfig: { + providers: [{ ...go, auth: { status: "authenticated", email: "same@example.com" } }], + }, + }); + input.set(EnvironmentId.make("env-b"), { + entry: { target: { label: "Desktop" } }, + serverConfig: { + providers: [ + { ...differentKey, auth: { status: "authenticated", email: "SAME@example.com" } }, + ], + }, + }); + expect(collectLimitAccounts(input)).toHaveLength(1); + }); + it("takes windows from a fresher hub read but credits and redeem from the native instance", () => { const native = provider({ driver: claude, diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index f2b5cfe53b84..ea4172c64b18 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -79,16 +79,23 @@ export type LimitPresentations = ReadonlyMap< } >; -function accountKey(driver: ServerProvider["driver"], email: string | undefined): string | null { +function accountKey( + driver: ServerProvider["driver"], + email: string | undefined, + limits?: ServerProviderUsageLimits, +): string | null { const normalizedEmail = email?.trim().toLowerCase(); - return normalizedEmail ? `${driver}:${normalizedEmail}` : null; + if (normalizedEmail) return `${driver}:${normalizedEmail}`; + return limits?.credentialFingerprint + ? `${driver}:credential:${limits.credentialFingerprint}` + : null; } /** * One subscription account as the pooled views see it, whichever way it was - * reported. The same email signed in natively on two environments, or reported - * by a hub as well as natively, is one account: its quota is one bucket, so - * counting it twice would misstate what is left. + * reported. Matching emails or credentials across environments name + * one account. Its quota is one bucket, so counting it twice would misstate + * what is left. */ export interface LimitAccount { readonly key: string; @@ -186,7 +193,7 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl for (const provider of providersWithLimits(presentation.serverConfig?.providers ?? [])) { if (!provider.usageLimits || limitsNotice(provider.usageLimits) !== null) continue; merge( - accountKey(provider.driver, provider.auth.email) ?? + accountKey(provider.driver, provider.auth.email, provider.usageLimits) ?? `${environmentId}:${provider.instanceId}`, { key: `${environmentId}:${provider.instanceId}`, @@ -214,27 +221,31 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl : source.label; for (const account of source.accounts) { if (limitsNotice(account.usageLimits) !== null) continue; - merge(accountKey(account.driver, account.email) ?? `${source.id}:${account.id}`, { - key: `${source.id}:${account.id}`, - driver: account.driver, - displayName: account.email ? null : account.id.replace(/\.json$/i, ""), - email: account.email, - plan: account.plan, - accentColor: undefined, - environments: [], - sourceLabel, - redeem: account.usageLimits.resetCredits?.nextCreditId - ? { - environmentId, - input: { - sourceId: source.id, - accountId: account.id, - creditId: account.usageLimits.resetCredits.nextCreditId, - }, - } - : null, - limits: account.usageLimits, - }); + merge( + accountKey(account.driver, account.email, account.usageLimits) ?? + `${source.id}:${account.id}`, + { + key: `${source.id}:${account.id}`, + driver: account.driver, + displayName: account.email ? null : account.id.replace(/\.json$/i, ""), + email: account.email, + plan: account.plan, + accentColor: undefined, + environments: [], + sourceLabel, + redeem: account.usageLimits.resetCredits?.nextCreditId + ? { + environmentId, + input: { + sourceId: source.id, + accountId: account.id, + creditId: account.usageLimits.resetCredits.nextCreditId, + }, + } + : null, + limits: account.usageLimits, + }, + ); } } } @@ -590,7 +601,7 @@ export function collectProviderUsageLimits( ); const nativeAccounts = new Set( native.flatMap((provider) => { - const key = accountKey(provider.driver, provider.auth.email); + const key = accountKey(provider.driver, provider.auth.email, provider.usageLimits); return key && provider.usageLimits?.windows.length && !provider.usageLimits.unavailable ? [key] : []; @@ -600,13 +611,13 @@ export function collectProviderUsageLimits( const notices: string[] = []; for (const provider of native) { if (!provider.usageLimits) continue; - const key = accountKey(provider.driver, provider.auth.email); + const key = accountKey(provider.driver, provider.auth.email, provider.usageLimits); const hubCredits = sources .flatMap((source) => source.accounts.map((account) => ({ source, account }))) .filter( ({ account }) => key !== null && - accountKey(account.driver, account.email) === key && + accountKey(account.driver, account.email, account.usageLimits) === key && account.usageLimits.resetCredits && !limitsNotice(account.usageLimits), ) @@ -653,7 +664,7 @@ export function collectProviderUsageLimits( for (const source of sources) { const matching = source.accounts.filter((account) => account.driver === selected.driver); for (const account of matching) { - const key = accountKey(account.driver, account.email); + const key = accountKey(account.driver, account.email, account.usageLimits); if (key && nativeAccounts.has(key)) continue; accounts.push({ id: `${source.id}:${account.id}`, From 23db5481fa5ec27dafbb8d69e6c5ee05c98b4979 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Mon, 28 Sep 2026 23:00:02 -0500 Subject: [PATCH 2/2] docs: explain OpenCode credential matching --- apps/server/src/provider/Layers/openCodeUsageLimits.ts | 5 +++-- packages/shared/src/usageLimits.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/openCodeUsageLimits.ts b/apps/server/src/provider/Layers/openCodeUsageLimits.ts index 4bbefd19551c..74d0bd75b9f8 100644 --- a/apps/server/src/provider/Layers/openCodeUsageLimits.ts +++ b/apps/server/src/provider/Layers/openCodeUsageLimits.ts @@ -98,8 +98,9 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")( ]; return { ...makeUsageLimits({ checkedAt, windows }), - // Go's usage response has no account ID, so identical keys are the only - // cross-environment identity we can establish from this probe. + // Go's usage response has no account ID. An unkeyed hash matches across + // environments without a shared secret. It permits offline guesses, but + // Go keys are randomly generated. credentialFingerprint: NodeCrypto.createHash("sha256") .update("opencode-go\0") .update(apiKey) diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index ea4172c64b18..f72a0f21df32 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -79,6 +79,7 @@ export type LimitPresentations = ReadonlyMap< } >; +/** Prefer the reported email; use an identical credential when no email is available. */ function accountKey( driver: ServerProvider["driver"], email: string | undefined,