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
15 changes: 15 additions & 0 deletions apps/server/src/t4/claudeAccounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ describe("t4 claudeAccounts", () => {
expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "main" });
});

it("with every account used up and no main login, gives the one that frees first (AI-99)", () => {
const { dir, env } = setup();
const home = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t4-home-"));
const noLogin = { ...env, HOME: home } as NodeJS.ProcessEnv;
markClaudeAccountExhausted("1", 90_000, 1_000, noLogin);
markClaudeAccountExhausted("2", 50_000, 1_000, noLogin);
expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "2", token: "token-two" });
// A token file gone is skipped there too.
NodeFS.rmSync(NodePath.join(dir, "2.token"));
expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "1", token: "token-one" });
// No usable account at all: the main login, as before.
NodeFS.rmSync(NodePath.join(dir, "1.token"));
expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "main" });
});

it("signs the status check and text generation in with an account, used up or not", () => {
const { dir, env } = setup();
const home = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t4-home-"));
Expand Down
18 changes: 13 additions & 5 deletions apps/server/src/t4/claudeAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ export const isClaudeAccountExhausted = (
return blockedUntil(loadState(env, files), key, model) > nowMs;
};

/** The first account in order not used up for `model` (the main login included), else the main login. */
/**
* The first account not used up for `model` (the main login included); with none, the main login
* if it exists, else the account freeing first - so Claude answers with its real limit, never the
* "Not logged in" of a token-less main, which nothing resumes (AI-99).
*/
export const pickClaudeAccount = (
nowMs: number,
env: NodeJS.ProcessEnv = process.env,
Expand All @@ -202,16 +206,20 @@ export const pickClaudeAccount = (
const files = accountFiles(env);
if (files.length === 0) return { key: MAIN_CLAUDE_ACCOUNT };
const state = loadState(env, files);
let soonest: { account: ClaudeAccount; until: number } | null = null;
for (const { key, file } of accountList(env)) {
if (blockedUntil(state, key, model) > nowMs) continue;
const until = blockedUntil(state, key, model);
if (file === undefined) {
if (mainLoginAvailable(env)) return { key: MAIN_CLAUDE_ACCOUNT };
if (until <= nowMs && mainLoginAvailable(env)) return { key: MAIN_CLAUDE_ACCOUNT };
continue;
}
const token = readToken(file);
if (token !== undefined) return { key, token };
if (token === undefined) continue;
if (until <= nowMs) return { key, token };
if (soonest === null || until < soonest.until) soonest = { account: { key, token }, until };
}
return { key: MAIN_CLAUDE_ACCOUNT };
if (mainLoginAvailable(env) || soonest === null) return { key: MAIN_CLAUDE_ACCOUNT };
return soonest.account;
};

export const claudeAccountEnvironment = (
Expand Down
Loading