Skip to content
Closed
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
45 changes: 25 additions & 20 deletions apps/server/src/provider/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
type ServerProvider,
type ServerProviderModel,
type ServerProviderSlashCommand,
type ServerProviderResetCredits,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as DateTime from "effect/DateTime";
Expand Down Expand Up @@ -52,6 +51,7 @@ import {
} from "./Drivers/ClaudeSkills.ts";
import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver";
import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits";
import type { ClaudeOAuthUsage, ClaudeOAuthUsageError } from "./claudeResetCredits.ts";
import {
type ClaudeScopedLimitNames,
claudeUsageResponseToLimits,
Expand Down Expand Up @@ -617,7 +617,9 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG,
/** Shared with the adapter so turn events reuse the scoped-bucket names this probe saw. */
scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>,
resolveResetCredits?: (version: string) => Effect.Effect<ServerProviderResetCredits | undefined>,
resolveOAuthUsage?: (
version: string,
) => Effect.Effect<ClaudeOAuthUsage | undefined, ClaudeOAuthUsageError>,
): Effect.fn.Return<
ServerProviderDraft,
never,
Expand Down Expand Up @@ -801,25 +803,28 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
subscriptionType: capabilities.subscriptionType,
authMethod: capabilities.tokenSource,
});
const usageCheckedAt = capabilities.usageCheckedAt ?? checkedAt;
const usageLimits = !capabilities.usage
? makeUnavailableUsageLimits({ checkedAt, reason: "probeFailed" })
: scopedLimitNames
? yield* recordClaudeUsageResponse(scopedLimitNames, {
response: capabilities.usage,
checkedAt: usageCheckedAt,
})
: claudeUsageResponseToLimits({
response: capabilities.usage,
checkedAt: usageCheckedAt,
}).limits;
const resetCredits =
resolveResetCredits &&
capabilities.subscriptionType &&
!usageLimits.unavailable &&
parsedVersion
? yield* resolveResetCredits(parsedVersion)
const oauthResult =
resolveOAuthUsage && capabilities.subscriptionType && parsedVersion
? yield* resolveOAuthUsage(parsedVersion).pipe(Effect.result)
: undefined;
const oauthUsage = oauthResult && Result.isSuccess(oauthResult) ? oauthResult.success : undefined;
const usage = oauthUsage?.usage ?? capabilities.usage;
Comment thread
merrkry marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
merrkry marked this conversation as resolved.
Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tie direct usage windows to the cached login identity

For file-backed logins, switching accounts externally in the same config directory and then refreshing Limits within five minutes now combines the new account's windows with the previous account's identity. ClaudeDriver caches capabilities, including email and workspaceId, for five minutes, while readClaudeOAuthUsage rereads .credentials.json on every refresh; the normal Limits-page refresh does not invalidate that cache. Previously the windows and identity came from the same cached probe. This replacement mislabels the new balance and can also overwrite the previous account's legitimate report through the email-keyed pooling in packages/shared/src/usageLimits.ts. Invalidate/reprobe capabilities when the credential identity changes before publishing these fresh windows.

Useful? React with 👍 / 👎.

const usageCheckedAt = oauthUsage
? DateTime.formatIso(yield* DateTime.now)
: (capabilities.usageCheckedAt ?? checkedAt);
const usageFailed = oauthResult && Result.isFailure(oauthResult) && !usage?.rate_limits_available;
// Only the SDK's filtered model list identifies the streamed overage bucket.
if (scopedLimitNames && capabilities.usage) {
yield* recordClaudeUsageResponse(scopedLimitNames, {
response: capabilities.usage,
checkedAt: usageCheckedAt,
});
}
const usageLimits =
!usage || usageFailed
? makeUnavailableUsageLimits({ checkedAt, reason: "probeFailed" })
: claudeUsageResponseToLimits({ response: usage, checkedAt: usageCheckedAt }).limits;
const resetCredits = oauthUsage?.resetCredits;
return buildServerProvider({
presentation: CLAUDE_PRESENTATION,
enabled: claudeSettings.enabled,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
catalog,
scopedLimitNames,
(version) =>
ClaudeResetCredits.readClaudeResetCredits(configDir, version).pipe(
ClaudeResetCredits.readClaudeOAuthUsage(configDir, version).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Expand Down
77 changes: 75 additions & 2 deletions apps/server/src/provider/ProviderInstanceRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PubSub from "effect/PubSub";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import { HttpClient, HttpClientResponse } from "effect/http";
Expand Down Expand Up @@ -713,9 +714,14 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
const client = HttpClient.make((request) =>
Effect.gen(function* () {
if (request.url.endsWith("/api/oauth/usage")) {
const redeemed = yield* fs.exists(marker);
if (redeemed && claim.usageFailsAfterClaim) {
return HttpClientResponse.fromWeb(request, new Response(null, { status: 500 }));
}
return HttpClientResponse.fromWeb(
request,
Response.json({
five_hour: { utilization: redeemed ? 0 : 100, resets_at: null },
cedar_ember: {
eligible: true,
next_grant_id: "grant_a",
Expand All @@ -725,11 +731,11 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
);
}
if (request.url.endsWith("/reset_rate_limits")) {
yield* fs.writeFileString(marker, "redeemed").pipe(Effect.orDie);
yield* fs.writeFileString(marker, "redeemed");
return HttpClientResponse.fromWeb(request, Response.json({ result: claim.result }));
}
return HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }));
}),
}).pipe(Effect.orDie),
);
const instanceId = ProviderInstanceId.make("claude_reset");
const { registry } = yield* makeProviderInstanceRegistry({
Expand Down Expand Up @@ -788,6 +794,73 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
}),
);

it.live.skipIf(windowsHost)("keeps Claude limits when a direct usage refresh fails", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const fixtures = yield* makeTildeProviderFixtures();
yield* fs.writeFileString(
path.join(fixtures.claudeHomePath, ".credentials.json"),
'{"claudeAiOauth":{"accessToken":"fake-token"}}',
);
const responseStatus = yield* Ref.make(200);
const utilization = yield* Ref.make(42);
const client = HttpClient.make((request) =>
Effect.gen(function* () {
if (!request.url.endsWith("/api/oauth/usage")) {
return HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }));
}
return HttpClientResponse.fromWeb(
request,
Response.json(
{
five_hour: { utilization: yield* Ref.get(utilization), resets_at: null },
cedar_ember: {
eligible: true,
next_grant_id: "grant_a",
grants: [{ id: "grant_a", resets_left: 2, usable_now: true }],
},
},
{ status: yield* Ref.get(responseStatus) },
),
);
}),
);
const instanceId = ProviderInstanceId.make("claude_direct_usage");
const { registry } = yield* makeProviderInstanceRegistry({
drivers: [ClaudeDriver],
configMap: {
[instanceId]: {
driver: ProviderDriverKind.make("claudeAgent"),
enabled: true,
environment: [
{ name: "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", value: "1", sensitive: false },
],
config: makeClaudeConfig({
enabled: true,
binaryPath: fixtures.claudeBinaryPath,
homePath: fixtures.claudeHomePath,
}),
},
},
}).pipe(Effect.provideService(HttpClient.HttpClient, client));
const instance = (yield* registry.getInstance(instanceId))!;
const before = yield* instance.snapshot.refresh;
expect(before.usageLimits?.windows[0]?.usedPercent).toBe(42);
expect(before.usageLimits?.resetCredits).toMatchObject({ availableCount: 2 });

yield* Ref.set(responseStatus, 429);
const failed = yield* instance.snapshot.refresh;
expect(failed.usageLimits).toEqual(before.usageLimits);

yield* Ref.set(responseStatus, 200);
yield* Ref.set(utilization, 25);
const recovered = yield* instance.snapshot.refresh;
expect(recovered.usageLimits?.windows[0]?.usedPercent).toBe(25);
expect(recovered.usageLimits?.unavailable).toBeUndefined();
}).pipe(Effect.provideService(HostProcess.Platform, "linux"), Effect.provide(layerTest)),
);

const makeClaudeCacheFixture = Effect.fnUntraced(function* (
state: "ready" | "signed-out" | "usage-failed",
) {
Expand Down
144 changes: 144 additions & 0 deletions apps/server/src/provider/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";
import { BUNDLED_CLAUDE_MODEL_CATALOG } from "./ClaudeModelCatalog.ts";
import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";
import { resolveUsageLimitsAfterProbe } from "@t3tools/provider-core/server/usageLimits";
import { type ClaudeOAuthUsage, ClaudeOAuthUsageError } from "./claudeResetCredits.ts";
import {
claudeOAuthUsageToResponse,
claudeRateLimitEventToUpdate,
makeClaudeScopedLimitNames,
} from "./claudeUsageLimits.ts";
import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
import * as AntigravityInstallation from "./AntigravityInstallation.ts";
import * as ModelManifest from "./ModelManifest.ts";
Expand Down Expand Up @@ -3888,6 +3895,143 @@ it.layer(
),
);

it.effect("reads usage and resets directly for subscription logins", () =>
Effect.gen(function* () {
const names = yield* makeClaudeScopedLimitNames;
const sdkUsage = {
rate_limits_available: true,
rate_limits: { five_hour: { utilization: 10, resets_at: null } },
};
const check = (
overrides: Partial<TestClaudeCapabilities & { usageCheckedAt: string }>,
oauthUsage?: Effect.Effect<ClaudeOAuthUsage | undefined, ClaudeOAuthUsageError>,
usage: ClaudeOAuthUsage["usage"] = sdkUsage,
) =>
checkClaudeProviderStatus(
defaultClaudeSettings,
() =>
Effect.succeed({
email: undefined,
subscriptionType: undefined,
tokenSource: undefined,
apiKeySource: undefined,
apiProvider: undefined,
slashCommands: [],
usage,
...overrides,
}),
undefined,
undefined,
undefined,
names,
oauthUsage ? () => oauthUsage : undefined,
);
const oauthUsage = {
usage: {
rate_limits_available: true,
rate_limits: { five_hour: { utilization: 42, resets_at: null } },
},
resetCredits: { availableCount: 2 },
};
// The CLI answers `get_usage` without windows when nonessential traffic is disabled.
const subscription = yield* check({ subscriptionType: "max" }, Effect.succeed(oauthUsage), {
rate_limits_available: false,
rate_limits: null,
});
const keychainLogin = yield* check({ subscriptionType: "max" });
const sdkFallback = yield* check(
{ subscriptionType: "max" },
Effect.fail(new ClaudeOAuthUsageError({ cause: new Error("request failed") })),
);
const unavailableKeychainLogin = yield* check({ subscriptionType: "max" }, undefined, {
rate_limits_available: false,
rate_limits: null,
});
const bedrock = yield* check({ apiProvider: "bedrock" }, Effect.succeed(oauthUsage));
assert.deepStrictEqual(
subscription.usageLimits?.windows.map((window) => window.usedPercent),
[42],
);
assert.deepStrictEqual(subscription.usageLimits?.resetCredits, { availableCount: 2 });
assert.deepStrictEqual(
keychainLogin.usageLimits?.windows.map((window) => window.usedPercent),
[10],
);
assert.strictEqual(keychainLogin.usageLimits?.resetCredits, undefined);
assert.deepStrictEqual(
sdkFallback.usageLimits?.windows.map((window) => window.usedPercent),
[10],
);
assert.strictEqual(
unavailableKeychainLogin.usageLimits?.unavailable?.reason,
"unsupported",
);
assert.deepStrictEqual(
bedrock.usageLimits?.windows.map((window) => window.usedPercent),
[10],
);
assert.strictEqual(bedrock.usageLimits?.resetCredits, undefined);

const sdkCheckedAt = DateTime.formatIso(yield* DateTime.now);
const freshOAuth = yield* check(
{ subscriptionType: "max", usageCheckedAt: sdkCheckedAt },
TestClock.adjust("1 second").pipe(Effect.as(oauthUsage)),
);
const cachedSdk = yield* check(
{ subscriptionType: "max", usageCheckedAt: sdkCheckedAt },
Effect.fail(new ClaudeOAuthUsageError({ cause: new Error("request failed") })),
);
assert.deepStrictEqual(
resolveUsageLimitsAfterProbe({
published: freshOAuth.usageLimits,
probed: cachedSdk.usageLimits,
})?.windows.map((window) => window.usedPercent),
[42],
);

const scopedUsage = (models: Array<string>) =>
claudeOAuthUsageToResponse({
limits: models.map((display_name) => ({
kind: "weekly_scoped",
percent: 7,
scope: { model: { display_name } },
})),
});
const direct = { usage: scopedUsage(["Other", "Fable"]), resetCredits: undefined };
const scoped = yield* check(
{ subscriptionType: "max" },
Effect.succeed(direct),
scopedUsage(["Fable"]),
);
assert.deepStrictEqual(
scoped.usageLimits?.windows.map((window) => window.id),
["seven_day_fable", "seven_day_other"],
);
const event = {
status: "allowed",
rateLimitType: "seven_day_overage_included" as never,
utilization: 0.5,
} as const;
assert.strictEqual(
claudeRateLimitEventToUpdate(event, yield* Ref.get(names))?.windows[0]?.id,
"seven_day_fable",
);
yield* check({ subscriptionType: "max" }, Effect.succeed(direct), {
rate_limits_available: false,
rate_limits: null,
});
assert.strictEqual(claudeRateLimitEventToUpdate(event, yield* Ref.get(names)), undefined);
}).pipe(
Effect.provide(
layerMockSpawner((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 };
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("advertises Ultracode as a toggle on custom models", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
Expand Down
Loading
Loading