Bill a turn to the node that asked, not the node that generated
Every turn runs on the AI owner now, so the ledger's per-host column had collapsed to a single name and the fleet breakdown said nothing. The asking node travels with the request and is validated before it is written, which also retires ai_token_sync.sh — a partner ledger is empty by construction.
This commit is contained in:
@@ -471,7 +471,6 @@
|
||||
# to 1 day). 30min was true "always fresh" but had no consumer that needed it that often.
|
||||
"Arrs_Stack/arr_cache_prefill.sh ARR_PREFILL_WAIT_MINUTES=1" # keep the shared arr tracked-data cache fresh
|
||||
"Arrs_Stack/arrs_failed_stalled_recovery.sh" # blocklist + re-search failed/stalled arr queue items
|
||||
"AI/ai_token_sync.sh" # pull partner AI token ledgers into the tmpfs cache
|
||||
)
|
||||
# arr_sync.sh runs as a fixed first step in intermediate_sync_maintenance.sh — not listed here.
|
||||
# It is controlled by ARR_SYNC_ENABLED (see Arr Sync section above).
|
||||
@@ -1966,12 +1965,13 @@
|
||||
AI_TOKEN_DB="${AI_DATA_DIR}/ai_token_history.db"
|
||||
AI_TOKEN_RETAIN_ROWS=20000 # oldest rows dropped past this — years of ordinary use
|
||||
|
||||
# AI/ai_token_sync.sh pulls each partner's ledger into the tmpfs cache the tab reads, so the
|
||||
# fleet total is a fleet total. Same trick conf_sync.sh uses for partner confs, minus the push:
|
||||
# nothing here is needed by anyone else, so the reader fetches its own data and controls its
|
||||
# own freshness. An unreachable partner is a quiet skip, not a warning — a partner is expected
|
||||
# to be down for long stretches, and a four-hourly warn trains you to ignore the script.
|
||||
AI_TOKEN_SYNC_ENABLED=true
|
||||
# AI/ai_token_sync.sh pulled each partner's ledger into a tmpfs cache so the fleet total was a
|
||||
# fleet total. The mesh shares one AI now: every turn generates on the owner and is recorded
|
||||
# there against the node that asked, so the owner's own ledger already holds the whole fleet and
|
||||
# a partner ledger is empty by construction. Left off the schedule rather than deleted — the
|
||||
# reader still merges any cache from before the change, which describes real turns.
|
||||
AI_TOKEN_SYNC_ENABLED=false
|
||||
|
||||
|
||||
# ━━━ AI Feature Toggles ━━━
|
||||
# Tier 1 is narration — it cannot change a decision. Tier 2 adds context to a decision a script
|
||||
|
||||
@@ -122,8 +122,17 @@ if ($explain) {
|
||||
exit(2);
|
||||
}
|
||||
} else {
|
||||
[$jobFile, $question, $historyJson, $kind, $think, $profile, $scope, $webArg] =
|
||||
array_slice($argv, 1, 8) + array_fill(0, 8, '');
|
||||
// Ninth argument is the node that asked, which is not necessarily the node generating: the
|
||||
// mesh shares one AI, so a turn started on a mirror runs here. Without it every row in the
|
||||
// ledger reads as the owner's and the tab can no longer say who spent what.
|
||||
//
|
||||
// Optional, and validated rather than trusted. It arrives from the RPC layer's own view of
|
||||
// the caller, but this is a command line and an unrecognisable value must fall back to "this
|
||||
// node" rather than write a slot name nobody can place.
|
||||
[$jobFile, $question, $historyJson, $kind, $think, $profile, $scope, $webArg, $askNode] =
|
||||
array_slice($argv, 1, 9) + array_fill(0, 9, '');
|
||||
|
||||
if (!preg_match('/^host\d+$/', (string)$askNode)) $askNode = '';
|
||||
|
||||
if ($jobFile === '' || $question === '') exit(1);
|
||||
if (!preg_match('#/[0-9a-f]{32}\.json$#', $jobFile)) exit(1);
|
||||
@@ -1239,7 +1248,8 @@ $tokS = $evalNs > 0 ? round($evalCount / ($evalNs / 1e9), 1) : null;
|
||||
|
||||
// Accounting before the job file is written, so a turn is counted even if the tab has already
|
||||
// been closed and nobody ever reads the result. Best-effort by contract — it cannot throw.
|
||||
vv_ai_token_record($profile, 'webgui', (int)($d['prompt_eval_count'] ?? 0), $evalCount, $tokS);
|
||||
vv_ai_token_record($profile, 'webgui', (int)($d['prompt_eval_count'] ?? 0), $evalCount, $tokS,
|
||||
$askNode ?? '');
|
||||
|
||||
// profile is the one that actually answered, not the one that was asked for — they differ on a
|
||||
// handoff. Reported so the page can show which contract produced the answer rather than the
|
||||
|
||||
@@ -872,16 +872,26 @@ function vv_ai_token_retain(): int {
|
||||
|
||||
// LOCK_EX because two turns can finish together — the composer allows a second question while
|
||||
// the first is still generating, and each runs in its own detached worker.
|
||||
function vv_ai_token_record(string $profile, string $source, int $prompt, int $completion, ?float $tokS): void {
|
||||
//
|
||||
// $node is who *asked*, which stopped being the same as who generated when the mesh began sharing
|
||||
// one AI: a turn started on a mirror runs on the owner and would otherwise be billed to the owner,
|
||||
// making the per-host breakdown a column of the owner's own name. Defaults to this node, so every
|
||||
// caller that does not know about the mesh keeps its previous meaning exactly.
|
||||
function vv_ai_token_record(string $profile, string $source, int $prompt, int $completion,
|
||||
?float $tokS, string $node = ''): void {
|
||||
if ($prompt <= 0 && $completion <= 0) return; // nothing generated; not a turn worth a row
|
||||
|
||||
$db = vv_ai_token_db();
|
||||
$dir = dirname($db);
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) return;
|
||||
|
||||
// Validated, not trusted: the host column is what the per-host totals group on, and a value
|
||||
// that is not a host slot would create a column nobody can place.
|
||||
$node = preg_match('/^host\d+$/', $node) ? $node : vv_detect_host();
|
||||
|
||||
$row = implode('|', [
|
||||
date('Y-m-d'), date('H:i:s'),
|
||||
vv_detect_host(),
|
||||
$node,
|
||||
preg_replace('/[^a-z0-9_-]/i', '', $profile) ?: 'unknown',
|
||||
preg_replace('/[^a-z0-9_-]/i', '', $source) ?: 'unknown',
|
||||
max(0, $prompt), max(0, $completion),
|
||||
@@ -938,13 +948,20 @@ function vv_ai_token_stats(): array {
|
||||
'synced' => null, 'today' => $blank, 'week' => $blank, 'all' => $blank];
|
||||
}
|
||||
|
||||
// Ledgers to read: our own, plus whatever AI/ai_token_sync.sh has pulled from partners.
|
||||
// Ledgers to read: our own, plus any partner ledger still cached from when each node kept its
|
||||
// own. The mesh shares one AI now — every turn generates on the owner and is recorded in the
|
||||
// owner's ledger against the node that asked — so on a current install this loop finds
|
||||
// nothing and the local file already holds the whole fleet.
|
||||
//
|
||||
// Kept rather than deleted: a cache from before the change still describes real turns, and
|
||||
// dropping the read would silently retire history the tab is showing today.
|
||||
//
|
||||
// Each entry carries the host slot it is allowed to contribute rows for. A partner file may
|
||||
// only add rows whose host column matches its filename — a ledger copied into the wrong slot,
|
||||
// or a partner that somehow cached ours, would otherwise be counted twice against a total
|
||||
// that still looked plausible. Our own file is trusted for any host, because it is the only
|
||||
// one written here and its host column is written by vv_detect_host().
|
||||
// one written here and its host column is written by vv_ai_token_record(), which validates
|
||||
// the slot before it writes one.
|
||||
$ledgers = [[$db, null]];
|
||||
foreach ((array)@glob(VV_AI_TOKEN_CACHE_DIR . '/host*.tokens.db') as $partnerDb) {
|
||||
if (!preg_match('/(host\d+)\.tokens\.db$/', $partnerDb, $m)) continue;
|
||||
|
||||
@@ -525,7 +525,11 @@ function vv_ai_dispatch(string $action, array $p, bool $isPost, int &$httpStatus
|
||||
. escapeshellarg($scope) . ' '
|
||||
// Asked for per turn. Only meaningful on a profile holding web_search — the worker
|
||||
// checks that, so a crafted web=1 against any other profile changes nothing.
|
||||
. escapeshellarg(($p['web'] ?? '') === '1' ? '1' : '0')
|
||||
. escapeshellarg(($p['web'] ?? '') === '1' ? '1' : '0') . ' '
|
||||
// Who asked. Set by the RPC layer for a forwarded turn and absent for a local one,
|
||||
// where the worker's own default is already correct. This is what keeps the token
|
||||
// ledger's per-host column meaningful once every turn generates on the owner.
|
||||
. escapeshellarg((string)($p['_vv_node'] ?? ''))
|
||||
. ' >/dev/null 2>&1 </dev/null &';
|
||||
$out = []; $rc = 0;
|
||||
exec($cmd, $out, $rc);
|
||||
|
||||
Reference in New Issue
Block a user