tokenmaxxing 1.156.0 → 1.158.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/check.ts +2 -0
- package/src/cli/key.ts +40 -11
- package/src/entries/pisupervisor.ts +7 -5
- package/src/entries/supervisor.ts +1 -1
- package/src/lib/apikeys.ts +59 -4
- package/src/lib/claude.ts +12 -15
- package/src/lib/pi.ts +5 -3
- package/src/lib/types.ts +1 -0
- package/src/main.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.158.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/check.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refreshOpenRouterCredit } from "../lib/apikeys.ts";
|
|
1
2
|
import { claude } from "../lib/claude.ts";
|
|
2
3
|
import { codex } from "../lib/codex.ts";
|
|
3
4
|
import { evaluateAndMaybeSwap, useBankedResets } from "../lib/decide.ts";
|
|
@@ -16,6 +17,7 @@ export async function cmdCheck(json = false): Promise<number> {
|
|
|
16
17
|
d = await evaluateAndMaybeSwap(claude, now);
|
|
17
18
|
await useBankedResets(codex, loadConfig(), now);
|
|
18
19
|
await sampleOldest(loadConfig());
|
|
20
|
+
await refreshOpenRouterCredit();
|
|
19
21
|
await maybeAutoUpdate();
|
|
20
22
|
} catch (e) {
|
|
21
23
|
const detail = errorMessage(e);
|
package/src/cli/key.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { keySessions, loadApiKeys, removeApiKeySecret, saveApiKeys, settleLiveSessionCosts, writeApiKeySecret } from "../lib/apikeys.ts";
|
|
2
|
+
import { isOpenRouterKey, keySessions, loadApiKeys, OPENROUTER_PREFIX, readOpenRouterCredit, removeApiKeySecret, saveApiKeys, settleLiveSessionCosts, writeApiKeySecret } from "../lib/apikeys.ts";
|
|
3
3
|
import { withLock } from "../lib/lock.ts";
|
|
4
|
+
import { errorMessage } from "../lib/log.ts";
|
|
4
5
|
import { claudePool } from "../lib/paths.ts";
|
|
5
6
|
import { readStdin } from "../lib/proc.ts";
|
|
6
7
|
import type { ApiKey } from "../lib/types.ts";
|
|
7
8
|
import { c, emitError, plain } from "./render.ts";
|
|
8
9
|
|
|
9
|
-
const USAGE =
|
|
10
|
+
const USAGE =
|
|
11
|
+
"usage: tokenmaxxing key add <label> [credit-usd] [workspace-id] (the key at the prompt, or on stdin when piped) | key credit <label> <usd> (an OpenRouter key takes no amount and clears its refusal) | key rm <label>";
|
|
10
12
|
|
|
11
13
|
const UsdSchema = z.string().trim().min(1).transform(Number).pipe(z.number().min(0));
|
|
12
14
|
|
|
@@ -44,13 +46,9 @@ async function add(args: string[]): Promise<number> {
|
|
|
44
46
|
return 2;
|
|
45
47
|
}
|
|
46
48
|
const workspaceId = workspace?.data;
|
|
47
|
-
|
|
48
|
-
emitError({ message: "refusing to read the API key from a terminal, which would echo it - pipe it in: read -rs KEY && printf '%s' \"$KEY\" | tokenmaxxing key add <label> ...; unset KEY" });
|
|
49
|
-
return 2;
|
|
50
|
-
}
|
|
51
|
-
const secret = (await readStdin()).trim();
|
|
49
|
+
const secret = (process.stdin.isTTY ? (prompt("API key:") ?? "") : await readStdin()).trim();
|
|
52
50
|
if (secret === "") {
|
|
53
|
-
emitError({ message: "no API key
|
|
51
|
+
emitError({ message: "no API key given - paste it at the prompt or pipe it in" });
|
|
54
52
|
return 2;
|
|
55
53
|
}
|
|
56
54
|
if (secret.includes("\n") || secret.includes("\r")) {
|
|
@@ -61,6 +59,19 @@ async function add(args: string[]): Promise<number> {
|
|
|
61
59
|
emitError({ message: "that is an Admin API key, which cannot run inference - create a regular API key in the Console" });
|
|
62
60
|
return 2;
|
|
63
61
|
}
|
|
62
|
+
let openrouter: ApiKey["openrouter"];
|
|
63
|
+
if (secret.startsWith(OPENROUTER_PREFIX)) {
|
|
64
|
+
if (credit != null || workspaceId != null) {
|
|
65
|
+
emitError({ message: "an OpenRouter key takes no credit-usd or workspace-id - its credit left is the key's limit_remaining on OpenRouter" });
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
openrouter = await readOpenRouterCredit(secret);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
emitError({ message: `could not read that OpenRouter key: ${errorMessage(e)}` });
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
return withLock(claudePool.lockFile, () => {
|
|
65
76
|
const idx = loadApiKeys();
|
|
66
77
|
const taken = findKey(idx.keys, label);
|
|
@@ -80,20 +91,24 @@ async function add(args: string[]): Promise<number> {
|
|
|
80
91
|
spentUsd: 0,
|
|
81
92
|
creditSetAt: now,
|
|
82
93
|
addedAt: now,
|
|
94
|
+
...(openrouter != null ? { openrouter } : {}),
|
|
83
95
|
});
|
|
84
96
|
saveApiKeys(idx);
|
|
85
|
-
|
|
97
|
+
const left = openrouter != null ? openrouter.remainingUsd : credit;
|
|
98
|
+
console.log(
|
|
99
|
+
`added ${openrouter != null ? "OpenRouter" : "API"} key ${c.bold(label)} (${left != null ? `credit ${usd(left)}` : "credit unknown, ranks after keys with a balance"}${workspaceId != null ? `, workspace ${workspaceId}` : ""})`,
|
|
100
|
+
);
|
|
86
101
|
return 0;
|
|
87
102
|
});
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
async function credit(args: string[]): Promise<number> {
|
|
91
106
|
const [label, amountRaw] = args;
|
|
92
|
-
if (!label ||
|
|
107
|
+
if (!label || args.length > 2) {
|
|
93
108
|
emitError({ message: USAGE, paint: plain });
|
|
94
109
|
return 2;
|
|
95
110
|
}
|
|
96
|
-
const amount = parseUsd(amountRaw);
|
|
111
|
+
const amount = amountRaw == null ? undefined : parseUsd(amountRaw);
|
|
97
112
|
if (amount === null) return 2;
|
|
98
113
|
return withLock(claudePool.lockFile, () => {
|
|
99
114
|
const idx = loadApiKeys();
|
|
@@ -102,6 +117,20 @@ async function credit(args: string[]): Promise<number> {
|
|
|
102
117
|
emitError({ message: `no API key is labeled "${label}"` });
|
|
103
118
|
return 1;
|
|
104
119
|
}
|
|
120
|
+
if (isOpenRouterKey(key)) {
|
|
121
|
+
if (amount != null) {
|
|
122
|
+
emitError({ message: `${key.label} is an OpenRouter key - its credit left is the key's limit_remaining, read from OpenRouter on every check tick; set the limit on OpenRouter. \`key credit ${key.label}\` with no amount clears a refusal` });
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
key.refusedAt = undefined;
|
|
126
|
+
saveApiKeys(idx);
|
|
127
|
+
console.log(`cleared the refusal on ${c.bold(key.label)}`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (amount == null) {
|
|
131
|
+
emitError({ message: USAGE, paint: plain });
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
105
134
|
key.creditUsd = amount;
|
|
106
135
|
key.spentUsd = 0;
|
|
107
136
|
key.creditSetAt = Date.now();
|
|
@@ -371,8 +371,10 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
|
371
371
|
const launched = await withLock(mover.pool.lockFile, async () => {
|
|
372
372
|
clearPresence({ dir: presenceDir, id });
|
|
373
373
|
if (terminating) return null;
|
|
374
|
-
const
|
|
375
|
-
|
|
374
|
+
const kept = validWanted(pool, wanted, id);
|
|
375
|
+
const target = kept ? { account: kept } : pickPiSeat(pool, launchedAt, model);
|
|
376
|
+
const picked = target?.account ?? null;
|
|
377
|
+
log("pisupervisor.launch", { id, pool, respawns, seat: picked?.id.slice(0, 8) ?? null, depletedUntil: target?.depletedUntil, args: launchArgs.join(" ") });
|
|
376
378
|
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
377
379
|
stdin: "inherit",
|
|
378
380
|
stdout: "inherit",
|
|
@@ -398,21 +400,21 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
|
398
400
|
log("pisupervisor.claim_release_failed", { err: errorMessage(e) });
|
|
399
401
|
}
|
|
400
402
|
}
|
|
401
|
-
return { proc: spawned, seat: picked };
|
|
403
|
+
return { proc: spawned, seat: picked, overriddenUntil: Math.max(overriddenUntil, target?.depletedUntil ?? 0) };
|
|
402
404
|
});
|
|
403
405
|
if (launched == null) {
|
|
404
406
|
await releaseClaim();
|
|
405
407
|
return 143;
|
|
406
408
|
}
|
|
407
409
|
|
|
408
|
-
const { proc, seat } = launched;
|
|
410
|
+
const { proc, seat, overriddenUntil: gateUntil } = launched;
|
|
409
411
|
const pending: { move: SwapDecision | null; killed: boolean } = { move: null, killed: false };
|
|
410
412
|
let seatCheckAt = 0;
|
|
411
413
|
await raceMarkerOrExit({
|
|
412
414
|
child: proc,
|
|
413
415
|
tick: async () => {
|
|
414
416
|
if (!seat || terminating || Date.now() < seatCheckAt) return false;
|
|
415
|
-
const watched = await watchSeat({ pool, seatId: seat.id, id, families, overriddenUntil });
|
|
417
|
+
const watched = await watchSeat({ pool, seatId: seat.id, id, families, overriddenUntil: gateUntil });
|
|
416
418
|
seatCheckAt = Date.now() + (watched.decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
417
419
|
pending.move = watched.move;
|
|
418
420
|
return watched.move !== null;
|
|
@@ -332,7 +332,7 @@ function systemLine(sid: string, text: string): string {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; onKey: boolean }): string {
|
|
335
|
-
const where = input.onKey ? "on an
|
|
335
|
+
const where = input.onKey ? "on an API key, because every pooled account is at its limit or the previous key ran out" : "on an account with quota headroom";
|
|
336
336
|
const moved = input.compacted
|
|
337
337
|
? `tokenmaxxing compacted this conversation and resumed the session ${where}.`
|
|
338
338
|
: `tokenmaxxing resumed this session ${where}.`;
|
package/src/lib/apikeys.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { chmodSync, closeSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { countBy, isEqual, sortBy } from "es-toolkit";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
|
+
import { http, safeErrorDetail } from "./http.ts";
|
|
5
7
|
import { withLock } from "./lock.ts";
|
|
6
|
-
import {
|
|
8
|
+
import { errorMessage, log } from "./log.ts";
|
|
9
|
+
import { claudePool, env, optionalEnv, paths } from "./paths.ts";
|
|
7
10
|
import { livingPresences } from "./presence.ts";
|
|
8
11
|
import type { ApiKeyPool } from "./provider.ts";
|
|
9
12
|
import { readJsonFile } from "./state.ts";
|
|
10
|
-
import { ApiKeysIndexSchema, SessionCostSchema, type ApiKey, type ApiKeysIndex, type SessionCost } from "./types.ts";
|
|
13
|
+
import { ApiKeysIndexSchema, JsonTextSchema, SessionCostSchema, type ApiKey, type ApiKeysIndex, type SessionCost } from "./types.ts";
|
|
11
14
|
|
|
12
15
|
export const API_KEY_ENV = "TOKENMAXXING_API_KEY_ID";
|
|
13
16
|
export const API_KEY_FD = 3;
|
|
@@ -15,7 +18,58 @@ export const API_KEY_FD = 3;
|
|
|
15
18
|
export function apiKeyEnv(env: Record<string, string | undefined>, key: ApiKey): Record<string, string | undefined> {
|
|
16
19
|
const header = key.workspaceId == null ? null : `anthropic-workspace-id: ${key.workspaceId}`;
|
|
17
20
|
const headers = header == null ? env.ANTHROPIC_CUSTOM_HEADERS : env.ANTHROPIC_CUSTOM_HEADERS ? `${env.ANTHROPIC_CUSTOM_HEADERS}\n${header}` : header;
|
|
18
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
...env,
|
|
23
|
+
CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR: String(API_KEY_FD),
|
|
24
|
+
...(headers != null ? { ANTHROPIC_CUSTOM_HEADERS: headers } : {}),
|
|
25
|
+
...(isOpenRouterKey(key) ? { ANTHROPIC_BASE_URL: OPENROUTER_URL } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const OPENROUTER_PREFIX = "sk-or-";
|
|
30
|
+
|
|
31
|
+
export function isOpenRouterKey(key: ApiKey): boolean {
|
|
32
|
+
return readApiKey(key).startsWith(OPENROUTER_PREFIX);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const OPENROUTER_URL = env("TOKENMAXXING_OPENROUTER_URL", "https://openrouter.ai/api");
|
|
36
|
+
const OPENROUTER_READ_DEADLINE_MS = 10_000;
|
|
37
|
+
const OpenRouterKeySchema = z.looseObject({ data: z.looseObject({ limit_remaining: z.number().nullable() }) });
|
|
38
|
+
|
|
39
|
+
export async function readOpenRouterCredit(secret: string): Promise<NonNullable<ApiKey["openrouter"]>> {
|
|
40
|
+
const at = Date.now();
|
|
41
|
+
const res = await http.get(`${OPENROUTER_URL}/v1/key`, { headers: { Authorization: `Bearer ${secret}` }, signal: AbortSignal.timeout(OPENROUTER_READ_DEADLINE_MS) });
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
if (!res.ok) throw new Error(`OpenRouter key read answered HTTP ${res.status}: ${safeErrorDetail({ text })}`);
|
|
44
|
+
return { remainingUsd: OpenRouterKeySchema.parse(JsonTextSchema.safeParse(text).data).data.limit_remaining, at };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function refreshOpenRouterCredit(): Promise<void> {
|
|
48
|
+
if (!existsSync(claudePool.apiKeysJson)) return;
|
|
49
|
+
const reads = await Promise.all(
|
|
50
|
+
availableKeys().map(async (k) => {
|
|
51
|
+
try {
|
|
52
|
+
if (!isOpenRouterKey(k)) return null;
|
|
53
|
+
return { id: k.id, credit: await readOpenRouterCredit(readApiKey(k).trim()) };
|
|
54
|
+
} catch (e) {
|
|
55
|
+
log("apikey.openrouter_read_failed", { key: k.id, err: errorMessage(e) });
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
const landed = reads.filter((r) => r != null);
|
|
61
|
+
if (landed.length === 0) return;
|
|
62
|
+
await withLock(claudePool.lockFile, () => {
|
|
63
|
+
const idx = loadApiKeys();
|
|
64
|
+
let changed = false;
|
|
65
|
+
for (const r of landed) {
|
|
66
|
+
const key = idx.keys.find((k) => k.id === r.id);
|
|
67
|
+
if (key == null || r.credit.at <= (key.openrouter?.at ?? 0)) continue;
|
|
68
|
+
key.openrouter = r.credit;
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
if (changed) saveApiKeys(idx);
|
|
72
|
+
});
|
|
19
73
|
}
|
|
20
74
|
|
|
21
75
|
export function readApiKey(key: ApiKey): string {
|
|
@@ -57,6 +111,7 @@ export function removeApiKeySecret(id: string): void {
|
|
|
57
111
|
}
|
|
58
112
|
|
|
59
113
|
export function creditLeft(k: ApiKey): number | null {
|
|
114
|
+
if (k.openrouter != null) return k.openrouter.remainingUsd;
|
|
60
115
|
return k.creditUsd == null ? null : k.creditUsd - k.spentUsd;
|
|
61
116
|
}
|
|
62
117
|
|
|
@@ -157,7 +212,7 @@ export async function foldSessionCost(sid: string, keyId: string | null, restore
|
|
|
157
212
|
const prev = idx.baselines[seen.sessionId];
|
|
158
213
|
if (prev != null && seen.usd <= prev.usd) continue;
|
|
159
214
|
const delta = Math.max(0, seen.usd - (prev?.usd ?? restoredCost(seen.sessionId)));
|
|
160
|
-
if (key) key.spentUsd += delta;
|
|
215
|
+
if (key && !isOpenRouterKey(key)) key.spentUsd += delta;
|
|
161
216
|
idx.baselines[seen.sessionId] = { usd: seen.usd, at: Date.now() };
|
|
162
217
|
changed = true;
|
|
163
218
|
}
|
package/src/lib/claude.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { withLock } from "./lock.ts";
|
|
|
10
10
|
import { errorMessage, log } from "./log.ts";
|
|
11
11
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
12
12
|
import { claudePool, env, paths, seatFromEnv, shortId, storeDirFor } from "./paths.ts";
|
|
13
|
-
import { MAX_WAITERS_PER_ACCOUNT, pickBest, pickEarliestReset, pickWaitTarget, thresholdBars, type
|
|
13
|
+
import { MAX_WAITERS_PER_ACCOUNT, pickBest, pickEarliestReset, pickWaitTarget, thresholdBars, type PickCtx } from "./picker.ts";
|
|
14
14
|
import { deletePiStore } from "./piauth.ts";
|
|
15
15
|
import { livingPresences, writePresence } from "./presence.ts";
|
|
16
16
|
import { StoreUnusableError, type Observation, type Provider, type ResetClaim, type SampleReport, type SeatBorrow } from "./provider.ts";
|
|
@@ -139,7 +139,9 @@ async function storeUsable(a: Account): Promise<boolean> {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
export type Seat = { account: Account; depletedUntil?: number };
|
|
143
|
+
|
|
144
|
+
export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Account) => boolean = () => true): Seat | null {
|
|
143
145
|
const cfg = loadConfig();
|
|
144
146
|
const bars = thresholdBars(cfg);
|
|
145
147
|
const idx = loadAccounts(claudePool);
|
|
@@ -149,27 +151,22 @@ function seatContext(now: number, model: ModelInfo | null, eligible: (a: Account
|
|
|
149
151
|
const seats = presence();
|
|
150
152
|
const ctx: PickCtx = { now, thresholds: bars, currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats };
|
|
151
153
|
const candidates = idx.accounts.filter(eligible);
|
|
152
|
-
const
|
|
154
|
+
const best = pickBest(candidates, ctx);
|
|
155
|
+
if (best) return { account: best };
|
|
156
|
+
const wait =
|
|
153
157
|
pickWaitTarget(candidates, ctx, seats, MAX_WAITERS_PER_ACCOUNT, now + cfg.policy.maxWaitMs, now + 2 * cfg.policy.maxWaitMs) ??
|
|
154
158
|
pickEarliestReset(candidates, ctx);
|
|
155
|
-
return {
|
|
159
|
+
return wait ? { account: wait.account, depletedUntil: wait.availableAt } : null;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
export
|
|
159
|
-
const { candidates, ctx, fallback } = seatContext(now, model, eligible);
|
|
160
|
-
return pickBest(candidates, ctx) ?? fallback()?.account ?? null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export type LaunchTarget = { account: Account; apiKey: null; depletedUntil?: number } | { account: null; apiKey: ApiKey };
|
|
162
|
+
export type LaunchTarget = (Seat & { apiKey: null }) | { account: null; apiKey: ApiKey };
|
|
164
163
|
|
|
165
164
|
export function pickLaunch(now: number, model: ModelInfo | null, refused: readonly string[] = []): LaunchTarget | null {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
if (best) return { account: best, apiKey: null };
|
|
165
|
+
const seat = pickSeat(now, model, (a) => !refused.includes(a.id));
|
|
166
|
+
if (seat && seat.depletedUntil === undefined) return { ...seat, apiKey: null };
|
|
169
167
|
const key = launchApiKey();
|
|
170
168
|
if (key) return { account: null, apiKey: key };
|
|
171
|
-
|
|
172
|
-
return wait ? { account: wait.account, apiKey: null, depletedUntil: wait.availableAt } : null;
|
|
169
|
+
return seat ? { ...seat, apiKey: null } : null;
|
|
173
170
|
}
|
|
174
171
|
|
|
175
172
|
export async function borrowClaudeSeat(pid: number, named?: string): Promise<SeatBorrow | { invalid: string }> {
|
package/src/lib/pi.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { claude, ensurePathAhead, pickSeat } from "./claude.ts";
|
|
3
|
+
import { claude, ensurePathAhead, pickSeat, type Seat } from "./claude.ts";
|
|
4
4
|
import { MAX_WRAP_DEPTH, PI_BIN, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
|
|
5
5
|
import { codex, pickCodexSeat } from "./codex.ts";
|
|
6
6
|
import { codexSeatIdOf } from "./codexauth.ts";
|
|
@@ -111,9 +111,11 @@ function piMover(base: Provider, pool: PiPool): Provider {
|
|
|
111
111
|
|
|
112
112
|
export const piMovers: Record<PiPool, Provider> = { claude: piMover(claude, "claude"), codex: piMover(codex, "codex") };
|
|
113
113
|
|
|
114
|
-
export function pickPiSeat(pool: PiPool, now: number, model: ModelInfo | null):
|
|
114
|
+
export function pickPiSeat(pool: PiPool, now: number, model: ModelInfo | null): Seat | null {
|
|
115
115
|
const eligible = (a: Account) => piStoreUsable(pool, a.id);
|
|
116
|
-
|
|
116
|
+
if (pool === "claude") return pickSeat(now, model, eligible);
|
|
117
|
+
const account = pickCodexSeat(now, eligible);
|
|
118
|
+
return account ? { account } : null;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
export function piPreflight(): void {
|
package/src/lib/types.ts
CHANGED
|
@@ -181,6 +181,7 @@ export const ApiKeySchema = z.object({
|
|
|
181
181
|
creditSetAt: z.number(),
|
|
182
182
|
refusedAt: z.number().optional(),
|
|
183
183
|
addedAt: z.number(),
|
|
184
|
+
openrouter: z.object({ remainingUsd: z.number().nullable(), at: z.number() }).optional(),
|
|
184
185
|
});
|
|
185
186
|
export type ApiKey = z.infer<typeof ApiKeySchema>;
|
|
186
187
|
|
package/src/main.ts
CHANGED
|
@@ -62,8 +62,8 @@ function printHelp(): void {
|
|
|
62
62
|
${c.cyan("tokenmaxxing doctor")} verify the install is intact
|
|
63
63
|
${c.cyan("tokenmaxxing rename")} [--codex | --grok] <sel> <label>
|
|
64
64
|
${c.cyan("tokenmaxxing rm")} [--codex | --grok] <sel>
|
|
65
|
-
${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic API key read from stdin; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces)
|
|
66
|
-
${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count
|
|
65
|
+
${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic or OpenRouter (sk-or-...) API key read from stdin or the prompt; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces; an OpenRouter key takes neither argument and reads its credit from OpenRouter)
|
|
66
|
+
${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count (an OpenRouter key takes no amount: key credit <label> clears its refusal)
|
|
67
67
|
${c.cyan("tokenmaxxing key rm")} <label> delete the key and its stored secret (refused while a session runs on it)
|
|
68
68
|
${c.cyan("tokenmaxxing seat")} <pid> lend one pooled Claude account to an unattended consumer until <pid> exits: prints the store directory to set as CLAUDE_SECURESTORAGE_CONFIG_DIR (the directory itself, as its owner, never a copy of its credential); host sessions and other borrowers share the account; exit 1 = none usable
|
|
69
69
|
${c.cyan("tokenmaxxing seat")} <pid> --store <id> lend the Claude store with that 8-character id instead of the ranked pick, checking only that it is pooled, holds a usable credential, and is not flagged for reauthentication; bars, the credits flag, and holds are the caller's job; exit 0 = granted, 1 = the store is unusable, 2 = unknown store, <pid> already holds another store, or bad usage
|