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
|
|
@ -133,28 +133,66 @@ export namespace Crafter::Review::SeatPool {
|
|||
// The other drivers' load on a seat as the last board fold left it.
|
||||
Json Foreign(const Json& seat);
|
||||
std::filesystem::path LedgerPath(const std::optional<std::filesystem::path>& scratchRoot = std::nullopt);
|
||||
// The most recent date with that day of the month, as YYYY-MM-DD.
|
||||
// The most recent date with that day of the month, as YYYY-MM-DD; a day
|
||||
// past a month's end is its last day.
|
||||
std::string CycleStart(double now, std::int32_t day);
|
||||
// When a seat's next credits are due, as YYYY-MM-DD: the grant day after
|
||||
// the current cycle's, or the current cycle's own when the seat went out
|
||||
// on credit before it (outSince) and is still out - due today, or overdue:
|
||||
// the grant day is wrong.
|
||||
std::string NextGrant(double now, std::int32_t day, const Json& outSince = nullptr);
|
||||
// The driver's verdict on a paid seat from its ledger entry alone: out
|
||||
// with the reason, or ok with none.
|
||||
std::pair<bool, std::string> LedgerVerdict(const Json& entry, double budget);
|
||||
// Why that verdict holds a seat out: "credit" (the key answered the
|
||||
// credit error), "budget", or null when it does not.
|
||||
Json LedgerOut(const Json& entry, double budget);
|
||||
|
||||
// What the driver spent on each API seat this cycle, and whether the key
|
||||
// answered the credit error; one file for every driver, each write a delta
|
||||
// applied under its lock.
|
||||
// applied under its lock. A seat's cycle starts on its grant day (days,
|
||||
// else the config's cycleDay).
|
||||
class Ledger {
|
||||
public:
|
||||
Ledger(std::filesystem::path path, Json apiConfig, Json data = Json::object());
|
||||
static Ledger Load(const std::filesystem::path& path, const Json& apiConfig);
|
||||
Ledger(std::filesystem::path path, Json apiConfig, Json data = Json::object(), std::map<std::string, std::int32_t> days = {});
|
||||
static Ledger Load(const std::filesystem::path& path, const Json& apiConfig, std::map<std::string, std::int32_t> days = {});
|
||||
std::int32_t Day(const std::string& seatId) const;
|
||||
void SetDay(const std::string& seatId, std::int32_t day);
|
||||
// The seat's entry of the current cycle. A new cycle starts the spend
|
||||
// over; a credit-out carries over it - only a probe that sees the key
|
||||
// answer again lifts it (Reconcile) - with the time the key was last
|
||||
// seen answering again. This process's copy: only an Update
|
||||
// re-reads the file.
|
||||
Json& Entry(const std::string& seatId, std::optional<double> now = std::nullopt);
|
||||
Json Add(const std::string& seatId, double usd, std::optional<double> now = std::nullopt);
|
||||
Json MarkOut(const std::string& seatId, std::string_view reason, std::optional<double> now = std::nullopt);
|
||||
// The probe's reading of an API key (probedAt: when the probe
|
||||
// started), and the ledger follows it, decided under the lock on the
|
||||
// entry as the file has it: "ok" on a key held out on credit since
|
||||
// before the probe started lifts the mark - an API key's is new
|
||||
// credits, GrantEntry's entry, "new-grant"; an OpenRouter key's keeps
|
||||
// its cycle's spend, "credit-back" -; the credit error on a key the file
|
||||
// has as fine, no grant seen since the probe started, is recorded,
|
||||
// "credit-gone". Anything else writes nothing, "": a mark another
|
||||
// driver made after the probe started may postdate its reading, and
|
||||
// a grant another driver already recorded keeps its spend.
|
||||
std::string Reconcile(const std::string& seatId, const Json& state, double probedAt, std::optional<double> now = std::nullopt);
|
||||
void Save();
|
||||
const Json& Data() const;
|
||||
const std::filesystem::path& Path() const;
|
||||
// The config the ledger was loaded with (ApiConfig's).
|
||||
const Json& ApiConfig() const;
|
||||
|
||||
private:
|
||||
std::filesystem::path path;
|
||||
Json apiConfig;
|
||||
Json data;
|
||||
Json Update(const std::string& seatId, const std::function<void(Json&)>& change, std::optional<double> now);
|
||||
std::map<std::string, std::int32_t> days;
|
||||
// change returns whether to write the file.
|
||||
Json Update(const std::string& seatId, const std::function<bool(Json&)>& change, std::optional<double> now);
|
||||
// A grant's entry: the cycle from the seat's grant day when that is
|
||||
// within the week, else from today; no spend, no out mark.
|
||||
Json GrantEntry(const std::string& seatId, double at) const;
|
||||
};
|
||||
|
||||
Json NewSeat(const std::string& id, const std::string& kind, const std::string& secret);
|
||||
|
|
@ -163,12 +201,23 @@ export namespace Crafter::Review::SeatPool {
|
|||
// local), local | api | oauth | openrouter,
|
||||
// or a list such as api1,1,2,local. std::invalid_argument with a
|
||||
// ValueError message for a seat that is not configured, or OpenRouter
|
||||
// mixed with the rest.
|
||||
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view spec, Ledger* ledger = nullptr, const Json& apiConfig = nullptr);
|
||||
// mixed with the rest. An API seat gets its grantDay
|
||||
// (ANTHROPIC_API_KEY<n>_GRANT_DAY of env, else the cycleDay) into the
|
||||
// seat and the ledger; a seat the ledger holds out is out, ledgerOut
|
||||
// "credit" or "budget".
|
||||
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view spec, Ledger* ledger = nullptr, const Json& apiConfig = nullptr,
|
||||
std::optional<double> now = std::nullopt);
|
||||
using ProbeFunction = std::function<Json(std::int32_t seat, const std::string& token, const std::string& model, double timeout, const std::string& claudeBin, bool local, bool api)>;
|
||||
// One probe per seat before the run; a seat already out is skipped.
|
||||
// An OpenRouter key's probe, ProbeOpenRouter's shape.
|
||||
using OpenRouterProbeFunction = std::function<Json(const std::string& secret, double timeout)>;
|
||||
// One probe per seat before the run; a seat already out is skipped,
|
||||
// except a paid key the ledger holds out on credit: probed again
|
||||
// (Reconcile). A probed key that answers with its cycle's budget spent
|
||||
// (an OpenRouter key's lift keeps its spend) is out on that budget. The
|
||||
// OpenRouter price catalogue is fetched when there are models to price.
|
||||
void Probe(std::vector<Json>& seats, const std::string& claudeBin, double timeout, const std::vector<std::string>& models,
|
||||
const std::function<void(const std::string&)>& log, const ProbeFunction& probe = {});
|
||||
const std::function<void(const std::string&)>& log, const ProbeFunction& probe = {}, Ledger* ledger = nullptr, std::optional<double> now = std::nullopt,
|
||||
const OpenRouterProbeFunction& probeKey = {});
|
||||
void ApplyProbe(Json& seat, const Json& result);
|
||||
std::string Describe(const Json& seat);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue