Skip to content
150 changes: 150 additions & 0 deletions apps/server/src/provider/codexUsageLimits.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
import { applyUsageLimitsUpdate } from "@t3tools/provider-core/server/usageLimits";
import { exhaustedUsageWindow } from "@t3tools/shared/usageLimits";
import * as CodexErrors from "effect-codex-app-server/errors";
import type { V2GetAccountRateLimitsResponse__RateLimitSnapshot } from "effect-codex-app-server/schema";
import { describe, expect, it } from "vite-plus/test";

import {
Expand Down Expand Up @@ -127,6 +131,98 @@ describe("codexRateLimitsToLimits", () => {
);
});

describe("Codex fallback routing", () => {
it("keeps an account with spendable credits after its weekly allowance runs out", () => {
const now = Date.parse(checkedAt);
const exhaustedWeek = {
limitId: "codex",
planType: "plus",
primary: { usedPercent: 12, resetsAt: 1_784_000_000, windowDurationMins: 300 },
secondary: { usedPercent: 100, resetsAt: 1_784_500_000, windowDurationMins: 10080 },
} satisfies V2GetAccountRateLimitsResponse__RateLimitSnapshot;
const exhaustedWindow = (
snapshot: V2GetAccountRateLimitsResponse__RateLimitSnapshot,
topLevel: V2GetAccountRateLimitsResponse__RateLimitSnapshot = snapshot,
) =>
exhaustedUsageWindow(
{
instanceId: ProviderInstanceId.make("codex"),
driver: ProviderDriverKind.make("codex"),
enabled: true,
installed: true,
version: null,
status: "ready",
auth: { status: "authenticated" },
checkedAt,
models: [],
slashCommands: [],
skills: [],
usageLimits: codexRateLimitsToLimits({
checkedAt,
snapshot: topLevel,
rateLimitsByLimitId: { codex: snapshot },
}),
} satisfies ServerProvider,
now,
"gpt-6-luna",
);
const credits = { hasCredits: true, unlimited: false, balance: "42.50" };

expect(exhaustedWindow({ ...exhaustedWeek, credits: null })?.id).toBe("secondary");
expect(
exhaustedWindow({
...exhaustedWeek,
credits: { hasCredits: false, unlimited: false, balance: "0" },
})?.id,
).toBe("secondary");
expect(exhaustedWindow({ ...exhaustedWeek, credits, spendControlReached: true })?.id).toBe(
"secondary",
);
expect(
exhaustedWindow({ ...exhaustedWeek, credits, spendControlReached: false }),
"weekly allowance used up but credits remain spendable",
).toBeUndefined();
expect(
exhaustedWindow({
...exhaustedWeek,
credits: { hasCredits: false, unlimited: true },
spendControlReached: false,
}),
"weekly allowance used up but credits are unlimited",
).toBeUndefined();
for (const rateLimitReachedType of [
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached",
] as const) {
expect(
exhaustedWindow({
...exhaustedWeek,
credits,
spendControlReached: null,
rateLimitReachedType,
})?.id,
rateLimitReachedType,
).toBe("secondary");
}
expect(
exhaustedWindow({
...exhaustedWeek,
secondary: { ...exhaustedWeek.secondary, usedPercent: 40 },
individualLimit: { limit: "50", used: "50", remainingPercent: 0, resetsAt: 1_784_500_000 },
credits,
spendControlReached: null,
})?.id,
"monthly credit limit used up",
).toBe("individual");
expect(
exhaustedWindow(exhaustedWeek, { ...exhaustedWeek, credits, spendControlReached: null }),
"credits reported only on the top-level snapshot",
).toBeUndefined();
});
});

describe("codexRateLimitsToUpdate", () => {
it("does not republish cached windows over a newer account probe", () => {
const cached = {
Expand Down Expand Up @@ -170,6 +266,60 @@ describe("codexRateLimitsToUpdate", () => {
expect(codexRateLimitsToUpdate({ planType: "plus" })).toBeUndefined();
});

it("only revokes the probe's credit grant, and only on an explicit block signal", () => {
const credits = { hasCredits: true, unlimited: false, balance: "42.50" };
const probe = {
limitId: "codex",
planType: "plus",
secondary: { usedPercent: 100, resetsAt: 1_784_500_000, windowDurationMins: 10080 },
credits,
spendControlReached: false,
} satisfies V2GetAccountRateLimitsResponse__RateLimitSnapshot;
// Codex sends null for policy fields it could not read on every ordinary notification.
const wire = {
limitId: "codex",
planType: "plus",
secondary: { usedPercent: 85, resetsAt: 1_784_500_000, windowDurationMins: 10080 },
credits,
spendControlReached: null,
individualLimit: null,
rateLimitReachedType: null,
} satisfies V2GetAccountRateLimitsResponse__RateLimitSnapshot;
const afterNotification = (
probed: V2GetAccountRateLimitsResponse__RateLimitSnapshot,
notification: V2GetAccountRateLimitsResponse__RateLimitSnapshot,
) =>
applyUsageLimitsUpdate({
previous: codexRateLimitsToLimits({ checkedAt, snapshot: probed }),
update: codexRateLimitsToUpdate(notification) ?? { windows: [] },
checkedAt: "2026-07-18T10:00:05.000Z",
})?.continuesOnCredits;

for (const blocked of [
{ ...probe, spendControlReached: true },
{ ...probe, rateLimitReachedType: "workspace_member_usage_limit_reached" as const },
{
...probe,
individualLimit: { limit: "50", used: "50", remainingPercent: 0, resetsAt: 1_784_500_000 },
},
]) {
expect(afterNotification(blocked, wire)).toBeUndefined();
}
expect(afterNotification(probe, { ...wire, credits: null })).toBe(true);
expect(afterNotification(probe, { ...wire, spendControlReached: true })).toBe(false);
expect(
afterNotification(probe, {
...wire,
credits: { hasCredits: false, unlimited: false, balance: "0" },
}),
).toBe(false);
expect(codexRateLimitsToUpdate({ spendControlReached: true })).toEqual({
windows: [],
continuesOnCredits: false,
});
expect(codexRateLimitsToUpdate({ ...wire, secondary: null })).toBeUndefined();
});

it("ignores Spark notifications so they cannot overwrite the main allowance", () => {
expect(
codexRateLimitsToUpdate({
Expand Down
50 changes: 43 additions & 7 deletions apps/server/src/provider/codexUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,24 @@ interface CodexRateLimitWindow {
export interface CodexRateLimitSnapshot {
readonly limitId?: string | null;
readonly planType?: string | null;
readonly rateLimitReachedType?: string | null;
readonly rateLimitReachedType?: string | null | undefined;
readonly primary?: CodexRateLimitWindow | null;
readonly secondary?: CodexRateLimitWindow | null;
readonly individualLimit?: {
readonly remainingPercent: number;
readonly resetsAt: number;
} | null;
readonly individualLimit?:
| {
readonly remainingPercent: number;
readonly resetsAt: number;
}
| null
| undefined;
readonly credits?:
| {
readonly hasCredits: boolean;
readonly unlimited: boolean;
}
| null
| undefined;
readonly spendControlReached?: boolean | null | undefined;
}

export interface CodexResetCreditsSummary {
Expand Down Expand Up @@ -113,6 +124,16 @@ function codexResetCreditsToContract(
};
}

/** Explicit block signals only: Codex sends null for fields it could not read, which says nothing. */
function codexCreditsBlocked(snapshot: CodexRateLimitSnapshot): boolean {
return (
snapshot.spendControlReached === true ||
snapshot.rateLimitReachedType?.startsWith("workspace_") === true ||
(snapshot.individualLimit != null && snapshot.individualLimit.remainingPercent <= 0) ||
(snapshot.credits != null && !snapshot.credits.hasCredits && !snapshot.credits.unlimited)
);
}

export function codexRateLimitsToLimits(input: {
readonly snapshot: CodexRateLimitSnapshot;
readonly rateLimitsByLimitId?:
Expand All @@ -123,13 +144,24 @@ export function codexRateLimitsToLimits(input: {
readonly checkedAt: string;
}): ServerProviderUsageLimits {
const resetCredits = codexResetCreditsToContract(input.resetCredits);
const windows = codexRateLimitsToWindows(input.rateLimitsByLimitId?.codex ?? input.snapshot);
const snapshot = input.rateLimitsByLimitId?.codex ?? input.snapshot;
// No fixture shows whether Codex reports credits on the bucket or the top-level snapshot.
const credits = snapshot.credits ?? input.snapshot.credits;
const continuesOnCredits =
(credits?.hasCredits === true || credits?.unlimited === true) &&
!codexCreditsBlocked({
...snapshot,
spendControlReached: snapshot.spendControlReached ?? input.snapshot.spendControlReached,
rateLimitReachedType: snapshot.rateLimitReachedType ?? input.snapshot.rateLimitReachedType,
individualLimit: snapshot.individualLimit ?? input.snapshot.individualLimit,
});
return {
...makeUsageLimits({
checkedAt: input.checkedAt,
windows,
windows: codexRateLimitsToWindows(snapshot),
}),
...(resetCredits ? { resetCredits } : {}),
...(continuesOnCredits ? { continuesOnCredits } : {}),
};
}

Expand All @@ -149,6 +181,10 @@ export function codexRateLimitsToUpdate(
existing.windowDurationMins !== window.windowDurationMins
);
});
// Only the account probe grants credit spending; a notification can only revoke it.
const revokesCredits =
(!snapshot.limitId || snapshot.limitId === "codex") && codexCreditsBlocked(snapshot);
if (revokesCredits) return { windows, continuesOnCredits: false };
return windows.length > 0 ? { windows } : undefined;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/providerUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const ServerProviderUsageLimits = Schema.Struct({
windows: ForwardCompatibleArray(ServerProviderUsageWindow),
credentialFingerprint: Schema.optional(TrimmedNonEmptyString),
resetCredits: Schema.optional(ServerProviderResetCredits),
/** True: the account keeps working on credits after its windows run out. False: revoked since the last probe. */
continuesOnCredits: Schema.optional(Schema.Boolean),
externalUsage: Schema.optional(
Schema.Struct({
label: TrimmedNonEmptyString,
Expand All @@ -52,6 +54,8 @@ export type ServerProviderUsageLimits = typeof ServerProviderUsageLimits.Type;
/** Partial updates merge windows by id; omitted windows remain unchanged. */
export const ProviderUsageLimitsUpdate = Schema.Struct({
windows: Schema.Array(ServerProviderUsageWindow),
/** False revokes the probe's credit grant; only a probe can grant it. */
continuesOnCredits: Schema.optional(Schema.Literal(false)),
});
export type ProviderUsageLimitsUpdate = typeof ProviderUsageLimitsUpdate.Type;

Expand Down
2 changes: 1 addition & 1 deletion packages/provider-core/src/server/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ManagedServerProvider {
readonly refresh: Effect.Effect<ServerProvider>;
readonly refreshUsage?: Effect.Effect<void, ServerSettingsError>;
readonly streamChanges: Stream.Stream<ServerProvider>;
/** Merge windows by ID; updates with no usable window leave the snapshot untouched. */
/** Merge windows by ID; updates that change no window and revoke no credit grant leave the snapshot untouched. */
readonly applyUsageLimits: (
update: ProviderUsageLimitsUpdate & { readonly checkedAt: string },
) => Effect.Effect<void>;
Expand Down
55 changes: 51 additions & 4 deletions packages/provider-core/src/server/usageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,53 @@ describe("applyUsageLimitsUpdate", () => {
).toEqual({ checkedAt, windows: [weekly], unavailable: { reason: "probeFailed" } });
});

it("preserves reset credits when a streamed window update changes usage", () => {
it("preserves reset credits and credit spending when a streamed window update changes usage", () => {
const resetCredits = { availableCount: 2, nextExpiresAt: "2026-10-01T00:00:00.000Z" };
const next = applyUsageLimitsUpdate({
previous: { ...published, resetCredits },
previous: { ...published, resetCredits, continuesOnCredits: true },
checkedAt: "2026-09-03T12:00:05.000Z",
update: { windows: [{ ...session, usedPercent: 55 }] },
update: { windows: [{ ...weekly, usedPercent: 100 }] },
});

expect(next).toEqual({
checkedAt: "2026-09-03T12:00:05.000Z",
windows: [{ ...session, usedPercent: 55 }, weekly],
windows: [session, { ...weekly, usedPercent: 100 }],
resetCredits,
continuesOnCredits: true,
});
});

it("records a credit block, even without a window change or a grant to revoke", () => {
const previous = { ...published, continuesOnCredits: true };
const checkedAtNext = "2026-09-03T12:00:05.000Z";
const revoke = (
limits: typeof published & { readonly continuesOnCredits?: boolean },
windows = [] as ReadonlyArray<typeof weekly>,
) =>
applyUsageLimitsUpdate({
previous: limits,
checkedAt: checkedAtNext,
update: { windows, continuesOnCredits: false },
});
const revoked = {
checkedAt: checkedAtNext,
windows: [session, weekly],
continuesOnCredits: false,
};
expect(revoke(previous)).toEqual(revoked);
expect(revoke(previous, [weekly])).toEqual(revoked);
// A probe in flight when the block lands must not grant over it.
const blocked = revoke(published);
expect(blocked).toEqual(revoked);
expect(
resolveUsageLimitsAfterProbe({
published: blocked,
probed: { ...published, continuesOnCredits: true },
})?.continuesOnCredits,
).toBe(false);
const alreadyRevoked = { ...published, continuesOnCredits: false };
expect(revoke(alreadyRevoked)).toBe(alreadyRevoked);
});
});

describe("resolveUsageLimitsAfterProbe", () => {
Expand Down Expand Up @@ -112,6 +145,20 @@ describe("resolveUsageLimitsAfterProbe", () => {
expect(resolveUsageLimitsAfterProbe({ published: live, probed: newer })).toEqual(newer);
});

it("keeps a revocation published after the probe started, and lets a newer probe reset it", () => {
const revoked = {
...published,
checkedAt: "2026-09-03T12:00:05.000Z",
continuesOnCredits: false,
};
const granted = { ...published, continuesOnCredits: true };
expect(
resolveUsageLimitsAfterProbe({ published: revoked, probed: granted })?.continuesOnCredits,
).toBe(false);
const newer = { ...granted, checkedAt: "2026-09-03T12:00:06.000Z" };
expect(resolveUsageLimitsAfterProbe({ published: revoked, probed: newer })).toBe(newer);
});

it("keeps the last good windows through a failed probe but not an unsupported one", () => {
const failed = { checkedAt, windows: [], unavailable: { reason: "probeFailed" as const } };
const unsupported = { checkedAt, windows: [], unavailable: { reason: "unsupported" as const } };
Expand Down
Loading
Loading