Skip to content
Open
6 changes: 5 additions & 1 deletion apps/server/src/provider/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,10 @@ export type ClaudeCapabilitiesProbe = {
readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
readonly models?: ReadonlyArray<ClaudeModelInfo>;
/** Undefined means get_usage failed; absent windows on success mean no subscription windows, as with API keys. */
readonly usage?: Pick<SDKControlGetUsageResponse, "rate_limits_available" | "rate_limits">;
readonly usage?: Pick<
SDKControlGetUsageResponse,
"rate_limits_available" | "rate_limits" | "subscription_type"
>;
readonly usageCheckedAt?: string;
};

Expand Down Expand Up @@ -432,6 +435,7 @@ const probeClaudeCapabilities = (
? {
rate_limits_available: usageResult.success.rate_limits_available,
rate_limits: usageResult.success.rate_limits,
subscription_type: usageResult.success.subscription_type,
}
: undefined;
const account = init.account as
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/provider/claudeUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ describe("claudeUsageResponseToLimits", () => {
]);
});

it("treats a subscription sign-in without readable limits as a failed probe, not a plan without limits", () => {
// Claude Max OAuth whose token lacks `user:profile`, as answered by Claude Code 2.1.316.
const subscriber = {
subscription_type: "max",
rate_limits_available: false,
rate_limits: null,
};
expect(
claudeUsageResponseToLimits({ checkedAt, response: subscriber }).limits.unavailable?.reason,
).toBe("probeFailed");
const apiKey = { ...subscriber, subscription_type: null };
expect(
claudeUsageResponseToLimits({ checkedAt, response: apiKey }).limits.unavailable?.reason,
).toBe("unsupported");
});

const enterpriseResponse = {
rate_limits_available: true,
rate_limits: {
Expand Down
15 changes: 14 additions & 1 deletion apps/server/src/provider/claudeUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,23 @@ export function claudeRateLimitEventToUpdate(

/** get_usage percentages are already 0-100. */
export function claudeUsageResponseToLimits(input: {
readonly response: Pick<SDKControlGetUsageResponse, "rate_limits_available" | "rate_limits">;
readonly response: Pick<SDKControlGetUsageResponse, "rate_limits_available" | "rate_limits"> &
Partial<Pick<SDKControlGetUsageResponse, "subscription_type">>;
readonly checkedAt: string;
}): { readonly limits: ServerProviderUsageLimits; readonly names: ClaudeScopedLimitNames } {
const { response, checkedAt } = input;
if (!response.rate_limits_available && response.subscription_type) {
// The CLI withholds limits from subscription tokens that lack the `user:profile` scope.
return {
limits: makeUnavailableUsageLimits({
checkedAt,
reason: "probeFailed",
message:
"Claude Code's sign-in can't read plan limits. Run `claude auth login` on the machine running T3 Code, using the Claude config directory from T3 Code's Claude provider settings if one is set, to sign in again.",
}),
names: { overageIncluded: undefined },
};
}
if (!response.rate_limits_available || !response.rate_limits) {
return {
limits: makeUnavailableUsageLimits({
Expand Down
Loading