Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeProvider.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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, []);
}
}),
Expand Down
12 changes: 11 additions & 1 deletion apps/server/src/provider/Layers/openCodeUsageLimits.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -95,7 +96,16 @@ 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. 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)
.digest("hex"),
};
}).pipe(
Effect.timeout("5 seconds"),
Effect.orElseSucceed(() =>
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/providerUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
/** Provider-owned usage settings when quota windows are not available to the client. */
externalUsage: Schema.optional(
Expand Down
68 changes: 68 additions & 0 deletions packages/shared/src/usageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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,
Expand Down
74 changes: 43 additions & 31 deletions packages/shared/src/usageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,24 @@ export function collectExternalUsageLinks(presentations: LimitPresentations) {
return [...links.values()];
}

function accountKey(driver: ServerProvider["driver"], email: string | undefined): string | null {
/** Prefer the reported email; use an identical credential when no email is available. */
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;
Expand Down Expand Up @@ -243,7 +251,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}`,
Expand Down Expand Up @@ -271,27 +279,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,
},
);
}
}
}
Expand Down Expand Up @@ -647,7 +659,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]
: [];
Expand All @@ -657,13 +669,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),
)
Expand Down Expand Up @@ -710,7 +722,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}`,
Expand Down
Loading