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
77 changes: 77 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,83 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("normalizes rate limit events into canonical windows", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;

const runtimeEventsFiber = yield* adapter.streamEvents.pipe(
Stream.takeUntil((event) => event.type === "session.exited"),
Stream.runCollect,
Effect.forkChild,
);

yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
runtimeMode: "full-access",
});

// Past the warning threshold the SDK reports utilization as a fraction.
harness.query.emit({
type: "rate_limit_event",
rate_limit_info: {
status: "allowed_warning",
resetsAt: 1_786_989_600,
rateLimitType: "seven_day",
utilization: 0.8,
isUsingOverage: false,
surpassedThreshold: 0.75,
},
session_id: "sdk-session-1",
uuid: "rate-limit-warning",
} as unknown as SDKMessage);

// Below the threshold the SDK omits utilization entirely.
harness.query.emit({
type: "rate_limit_event",
rate_limit_info: {
status: "allowed",
resetsAt: 1_786_995_000,
rateLimitType: "five_hour",
isUsingOverage: false,
},
session_id: "sdk-session-1",
uuid: "rate-limit-allowed",
} as unknown as SDKMessage);

// No rateLimitType means no window to key on; the event is dropped.
harness.query.emit({
type: "rate_limit_event",
rate_limit_info: { status: "allowed" },
session_id: "sdk-session-1",
uuid: "rate-limit-untyped",
} as unknown as SDKMessage);

harness.query.finish();

const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
const rateLimitEvents = runtimeEvents.filter(
(event) => event.type === "account.rate-limits.updated",
);
assert.equal(rateLimitEvents.length, 2);
const [warning, allowed] = rateLimitEvents;
if (warning?.type === "account.rate-limits.updated") {
assert.deepEqual(warning.payload, {
windows: [{ id: "seven_day", usedPercent: 80, resetsAt: 1_786_989_600 }],
});
}
if (allowed?.type === "account.rate-limits.updated") {
assert.deepEqual(allowed.payload, {
windows: [{ id: "five_hour", resetsAt: 1_786_995_000 }],
});
}
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("steers a running turn instead of opening a new one on mid-turn sendTurn", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
40 changes: 33 additions & 7 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import {
type SDKAssistantMessageError,
type SDKMessage,
type SDKControlGetContextUsageResponse,
type SDKRateLimitInfo,
type SDKResultMessage,
type SettingSource,
type SDKUserMessage,
type ModelUsage,
} from "@anthropic-ai/claude-agent-sdk";
import { parseCliArgs } from "@t3tools/shared/cliArgs";
import {
type AccountRateLimitsUpdatedPayload,
ApprovalRequestId,
type CanonicalItemType,
type CanonicalRequestType,
Expand Down Expand Up @@ -351,6 +353,29 @@ function normalizeClaudeStreamMessages(
return squashed.length > 0 ? [squashed] : [];
}

// The SDK reports utilization as a 0-1 fraction and omits it entirely while
// status is "allowed"; the canonical payload uses 0-100 percent and treats the
// absent value as "not reported". Events without a rateLimitType carry nothing
// a consumer could key a window on, so they normalize to undefined.
function normalizeClaudeRateLimits(
info: SDKRateLimitInfo,
): AccountRateLimitsUpdatedPayload | undefined {
if (!info.rateLimitType) {
return undefined;
}
return {
windows: [
{
id: info.rateLimitType,
...(info.utilization !== undefined
? { usedPercent: Math.round(info.utilization * 1000) / 10 }
: {}),
...(info.resetsAt !== undefined ? { resetsAt: info.resetsAt } : {}),
},
],
};
}

function getEffectiveClaudeAgentEffort(
effort: string | null | undefined,
model: string | null | undefined,
Expand Down Expand Up @@ -3564,13 +3589,14 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
}

if (message.type === "rate_limit_event") {
yield* offerRuntimeEvent({
...base,
type: "account.rate-limits.updated",
payload: {
rateLimits: message,
},
});
const payload = normalizeClaudeRateLimits(message.rate_limit_info);
if (payload) {
yield* offerRuntimeEvent({
...base,
type: "account.rate-limits.updated",
payload,
});
}
return;
}
});
Expand Down
68 changes: 68 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,74 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
}),
);

it.effect("normalizes rate limit snapshots into canonical windows", () =>
Effect.gen(function* () {
const { adapter, runtime } = yield* startLifecycleRuntime();
const eventsFiber = yield* Stream.take(adapter.streamEvents, 2).pipe(
Stream.runCollect,
Effect.forkChild,
);

yield* runtime.emit({
id: asEventId("evt-codex-rate-limits-full"),
kind: "notification",
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-1"),
createdAt: "2026-01-01T00:00:00.000Z",
method: "account/rateLimits/updated",
payload: {
rateLimits: {
credits: { balance: "0", hasCredits: false, unlimited: false },
individualLimit: null,
limitId: "codex",
limitName: null,
planType: "pro",
primary: { resetsAt: 1_787_581_395, usedPercent: 3, windowDurationMins: 10_080 },
rateLimitReachedType: null,
secondary: null,
spendControlReached: null,
},
},
} satisfies ProviderEvent);

// Sparse rolling update: one window, all account metadata unavailable.
yield* runtime.emit({
id: asEventId("evt-codex-rate-limits-sparse"),
kind: "notification",
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("thread-1"),
turnId: asTurnId("turn-1"),
createdAt: "2026-01-01T00:00:01.000Z",
method: "account/rateLimits/updated",
payload: {
rateLimits: {
primary: { usedPercent: 4 },
},
},
} satisfies ProviderEvent);

const events = Array.from(yield* Fiber.join(eventsFiber));
NodeAssert.equal(events[0]?.type, "account.rate-limits.updated");
if (events[0]?.type === "account.rate-limits.updated") {
NodeAssert.deepEqual(events[0].payload, {
windows: [
{ id: "primary", usedPercent: 3, resetsAt: 1_787_581_395, windowMinutes: 10_080 },
],
limitId: "codex",
planType: "pro",
credits: { balance: "0", hasCredits: false, unlimited: false },
});
}
NodeAssert.equal(events[1]?.type, "account.rate-limits.updated");
if (events[1]?.type === "account.rate-limits.updated") {
NodeAssert.deepEqual(events[1].payload, {
windows: [{ id: "primary", usedPercent: 4 }],
});
}
}),
);

// Production calls startSession from a request fiber that finishes as soon as
// the session exists. `Effect.forkChild` made the runtime event consumer a
// child of that fiber, and Effect interrupts a fiber's children when it
Expand Down
61 changes: 57 additions & 4 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* @module CodexAdapterLive
*/
import {
type AccountRateLimitsUpdatedPayload,
type AccountRateLimitWindow,
type CanonicalItemType,
type CanonicalRequestType,
type CodexSettings,
Expand Down Expand Up @@ -193,6 +195,51 @@ function normalizeCodexTokenUsage(
};
}

// Codex sends sparse rolling updates: null/absent fields mean "not included
// here", never "cleared", so only what actually arrived is forwarded.
function normalizeCodexRateLimits(
snapshot: EffectCodexSchema.V2AccountRateLimitsUpdatedNotification["rateLimits"],
): AccountRateLimitsUpdatedPayload | undefined {
const windows: Array<AccountRateLimitWindow> = [];
for (const id of ["primary", "secondary"] as const) {
const window = snapshot[id];
if (!window) {
continue;
}
windows.push({
id,
usedPercent: window.usedPercent,
...(window.resetsAt != null ? { resetsAt: window.resetsAt } : {}),
...(window.windowDurationMins != null ? { windowMinutes: window.windowDurationMins } : {}),
});
}

const limitId = trimText(snapshot.limitId);
const limitName = trimText(snapshot.limitName);
const planType = trimText(snapshot.planType);
const credits = snapshot.credits
? {
...(trimText(snapshot.credits.balance)
? { balance: trimText(snapshot.credits.balance) }
: {}),
hasCredits: snapshot.credits.hasCredits,
unlimited: snapshot.credits.unlimited,
}
: undefined;

if (windows.length === 0 && !planType && !credits) {
return undefined;
}

return {
windows,
...(limitId ? { limitId } : {}),
...(limitName ? { limitName } : {}),
...(planType ? { planType } : {}),
...(credits ? { credits } : {}),
};
}

function toTurnStatus(
value: EffectCodexSchema.V2TurnCompletedNotification["turn"]["status"] | "cancelled",
): "completed" | "failed" | "cancelled" | "interrupted" {
Expand Down Expand Up @@ -1408,16 +1455,22 @@ function mapToRuntimeEvents(
}

if (event.method === "account/rateLimits/updated") {
if (!readPayload(EffectCodexSchema.V2AccountRateLimitsUpdatedNotification, event.payload)) {
const payload = readPayload(
EffectCodexSchema.V2AccountRateLimitsUpdatedNotification,
event.payload,
);
if (!payload) {
return [];
}
const rateLimits = normalizeCodexRateLimits(payload.rateLimits);
if (!rateLimits) {
return [];
}
return [
{
type: "account.rate-limits.updated",
...runtimeEventBase(event, canonicalThreadId),
payload: {
rateLimits: event.payload ?? {},
},
payload: rateLimits,
},
];
}
Expand Down
66 changes: 66 additions & 0 deletions apps/server/src/provider/Layers/CodexProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, it } from "@effect/vitest";

import {
applyPreferredCodexDefaultModel,
codexServerRateLimits,
isLegacyCodexModel,
mapCodexModelCapabilities,
} from "./CodexProvider.ts";
Expand Down Expand Up @@ -163,3 +164,68 @@ it("ignores custom models that shadow a preferred slug", () => {

assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
});

it("flattens a single-limit rate limits read into canonical windows", () => {
const rateLimits = codexServerRateLimits(
{
rateLimits: {
credits: { balance: "0", hasCredits: false, unlimited: false },
limitId: "codex",
limitName: null,
planType: "pro",
primary: { resetsAt: 1_787_581_395, usedPercent: 3, windowDurationMins: 10_080 },
secondary: null,
},
},
"2026-01-01T00:00:00.000Z",
);

assert.deepStrictEqual(rateLimits, {
windows: [{ id: "primary", usedPercent: 3, resetsAt: 1_787_581_395, windowMinutes: 10_080 }],
updatedAt: "2026-01-01T00:00:00.000Z",
});
});

it("ignores per-model named limits so the card shows only account quota", () => {
const rateLimits = codexServerRateLimits(
{
rateLimits: {
limitId: "codex",
limitName: null,
primary: { usedPercent: 7, windowDurationMins: 10_080, resetsAt: 1_787_581_395 },
secondary: null,
},
// Backend key order here is not guaranteed, so a Spark quota could
// otherwise render ahead of the account's own limit.
rateLimitsByLimitId: {
codex_bengalfox: {
limitId: "codex_bengalfox",
limitName: "GPT-5.3-Codex-Spark",
primary: { usedPercent: 0, windowDurationMins: 10_080, resetsAt: 1_787_679_023 },
},
codex: {
limitId: "codex",
limitName: null,
primary: { usedPercent: 7, windowDurationMins: 10_080, resetsAt: 1_787_581_395 },
},
},
},
"2026-01-01T00:00:00.000Z",
);

assert.deepStrictEqual(rateLimits, {
windows: [{ id: "primary", usedPercent: 7, resetsAt: 1_787_581_395, windowMinutes: 10_080 }],
updatedAt: "2026-01-01T00:00:00.000Z",
});
});

it("reports no rate limits when the response has no windows", () => {
assert.deepStrictEqual(
codexServerRateLimits(
{ rateLimits: { primary: null, secondary: null } },
"2026-01-01T00:00:00.000Z",
),
undefined,
);
assert.deepStrictEqual(codexServerRateLimits(undefined, "2026-01-01T00:00:00.000Z"), undefined);
});
Loading
Loading