feat(seats): an API seat's cycle starts on its own grant day
The ledger reset every API seat's spend on one REVIEW_GATES_API_CYCLE_DAY and cleared every credit-out, an OpenRouter key's too, at that reset. Each API seat's cycle starts on its ANTHROPIC_API_KEY<n>_GRANT_DAY now (1..31, a day past a month's end its last), else the cycle day, also 1..31. A credit-out carries across the rollover, and the run's probe tries a key held out on credit again, as `seats` does an API key: an ok lifts the mark - an API key's is new credits, its cycle restarting from the grant day, or from today when that day is over a week back; an OpenRouter key keeps its cycle's spend, and a key that answers with its cycle's budget spent stays out on that budget. A probe that finds a key out of credit records it, unless the ledger already has it out. Both are decided in one locked update on the file's entry, a mark another driver made since the probe started standing, and the ledger's stamps carry their UTC offset so marks are ordered by instant. `seats` shows the probe's reading beside the driver's verdict (the `probe` and `driver` columns replace `state` and `ledger: out`), each seat's grant day and when its next credits are due, and notes on what the probe changed in the ledger or why the driver's verdict stands; the run logs the days.
This commit is contained in:
parent
c6b4d78245
commit
ceb87f70ef
12 changed files with 774 additions and 83 deletions
|
|
@ -534,7 +534,8 @@ inventory unit, against a frozen commit range.)";
|
|||
constexpr std::string_view Prog = "crafter-review seats";
|
||||
const Parsed parsed = ParseOptions(args, SeatsOptions);
|
||||
if (parsed.help) {
|
||||
std::print("{}\nSeats: CLAUDE_CODE_OAUTH_TOKEN1..6 from the environment and/or the dotenv file. Never prints a token.\n",
|
||||
std::print("{}\nSeats: CLAUDE_CODE_OAUTH_TOKEN1..6 and ANTHROPIC_API_KEY1..6 from the environment and/or the dotenv file. Never prints a token. An API key's "
|
||||
"probe is recorded in the seat ledger: new credits, or the credit error.\n",
|
||||
Help(Prog, "How much usage each Claude seat has left: every seat probed, both windows and their resets.", SeatsOptions));
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -603,9 +604,23 @@ inventory unit, against a frozen commit range.)";
|
|||
Seats::ProbeOptions api = haiku;
|
||||
api.api = true;
|
||||
apiSeats = ProbeAll(todo, api);
|
||||
// The driver's ledger: what it spent on each key this cycle.
|
||||
SeatPool::Ledger ledger = SeatPool::Ledger::Load(SeatPool::LedgerPath(), apiConfig);
|
||||
for (Json& s : apiSeats) s["ledger"] = ledger.Entry(std::format("api{}", s.at("seat").get<std::int64_t>()));
|
||||
// The driver's ledger: what it spent on each key this cycle,
|
||||
// from each seat's grant day. It follows the probe: a key that
|
||||
// answers again after the credit error is new credits, the
|
||||
// credit error on a key it has as fine is recorded.
|
||||
std::map<std::string, std::int32_t> days;
|
||||
for (const auto& [n, day] : Seats::ApiGrantDays(env)) days[std::format("api{}", n)] = day;
|
||||
SeatPool::Ledger ledger = SeatPool::Ledger::Load(SeatPool::LedgerPath(), apiConfig, days);
|
||||
for (Json& s : apiSeats) {
|
||||
const std::string id = std::format("api{}", s.at("seat").get<std::int64_t>());
|
||||
s["grantDay"] = ledger.Day(id);
|
||||
// `now`, taken before the probes started, is their start;
|
||||
// what Reconcile writes is stamped when it is written.
|
||||
s["event"] = ledger.Reconcile(id, s.at("state"), now);
|
||||
s["ledger"] = ledger.Entry(id, now);
|
||||
s["ledgerOut"] = SeatPool::LedgerOut(s.at("ledger"), apiConfig.at("budgetUsd").get<double>());
|
||||
s["nextCredits"] = SeatPool::NextGrant(now, ledger.Day(id), Dyn::Get(s.at("ledger"), "outSince"));
|
||||
}
|
||||
}
|
||||
// The board: the drivers running now and what they hold on each
|
||||
// account - read, never written.
|
||||
|
|
@ -631,7 +646,7 @@ inventory unit, against a frozen commit range.)";
|
|||
for (const Json& s : apiSeats) ids.push_back(std::format("api{}", s.at("seat").get<std::int64_t>()));
|
||||
std::println("seat pool: {} seat(s) ({}) from {}, probed {} {}, claude: {}\n", ids.size(), Dyn::Join(ids, ", "), source, Seats::Clock(now, zone), zone.label, claudeBin);
|
||||
if (!seats.empty()) std::println("{}", Seats::RenderStatusReport(seats, cfg, now, zone, board, keys));
|
||||
if (!apiSeats.empty()) std::println("{}{}", seats.empty() ? "" : "\n", Seats::RenderApiReport(apiSeats, apiConfig, board, apiKeys));
|
||||
if (!apiSeats.empty()) std::println("{}{}", seats.empty() ? "" : "\n", Seats::RenderApiReport(apiSeats, apiConfig, now, board, apiKeys));
|
||||
std::println("\n{}", Seats::RenderDrivers(board, now, zone));
|
||||
// The pids above are no use from inside the sandbox: the way to
|
||||
// stop a run instead.
|
||||
|
|
|
|||
Loading…
Reference in a new issue