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.
|
# 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/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
|
"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.
|
# 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).
|
# 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_DB="${AI_DATA_DIR}/ai_token_history.db"
|
||||||
AI_TOKEN_RETAIN_ROWS=20000 # oldest rows dropped past this — years of ordinary use
|
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
|
# AI/ai_token_sync.sh pulled each partner's ledger into a tmpfs cache so the fleet total was a
|
||||||
# fleet total is a fleet total. Same trick conf_sync.sh uses for partner confs, minus the push:
|
# fleet total. The mesh shares one AI now: every turn generates on the owner and is recorded
|
||||||
# nothing here is needed by anyone else, so the reader fetches its own data and controls its
|
# there against the node that asked, so the owner's own ledger already holds the whole fleet and
|
||||||
# own freshness. An unreachable partner is a quiet skip, not a warning — a partner is expected
|
# a partner ledger is empty by construction. Left off the schedule rather than deleted — the
|
||||||
# to be down for long stretches, and a four-hourly warn trains you to ignore the script.
|
# reader still merges any cache from before the change, which describes real turns.
|
||||||
AI_TOKEN_SYNC_ENABLED=true
|
AI_TOKEN_SYNC_ENABLED=false
|
||||||
|
|
||||||
|
|
||||||
# ━━━ AI Feature Toggles ━━━
|
# ━━━ AI Feature Toggles ━━━
|
||||||
# Tier 1 is narration — it cannot change a decision. Tier 2 adds context to a decision a script
|
# 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);
|
exit(2);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
[$jobFile, $question, $historyJson, $kind, $think, $profile, $scope, $webArg] =
|
// Ninth argument is the node that asked, which is not necessarily the node generating: the
|
||||||
array_slice($argv, 1, 8) + array_fill(0, 8, '');
|
// 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 ($jobFile === '' || $question === '') exit(1);
|
||||||
if (!preg_match('#/[0-9a-f]{32}\.json$#', $jobFile)) 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// 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.
|
// 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
|
if ($prompt <= 0 && $completion <= 0) return; // nothing generated; not a turn worth a row
|
||||||
|
|
||||||
$db = vv_ai_token_db();
|
$db = vv_ai_token_db();
|
||||||
$dir = dirname($db);
|
$dir = dirname($db);
|
||||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) return;
|
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('|', [
|
$row = implode('|', [
|
||||||
date('Y-m-d'), date('H:i:s'),
|
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', '', $profile) ?: 'unknown',
|
||||||
preg_replace('/[^a-z0-9_-]/i', '', $source) ?: 'unknown',
|
preg_replace('/[^a-z0-9_-]/i', '', $source) ?: 'unknown',
|
||||||
max(0, $prompt), max(0, $completion),
|
max(0, $prompt), max(0, $completion),
|
||||||
@@ -938,13 +948,20 @@ function vv_ai_token_stats(): array {
|
|||||||
'synced' => null, 'today' => $blank, 'week' => $blank, 'all' => $blank];
|
'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
|
// 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,
|
// 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
|
// 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
|
// 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]];
|
$ledgers = [[$db, null]];
|
||||||
foreach ((array)@glob(VV_AI_TOKEN_CACHE_DIR . '/host*.tokens.db') as $partnerDb) {
|
foreach ((array)@glob(VV_AI_TOKEN_CACHE_DIR . '/host*.tokens.db') as $partnerDb) {
|
||||||
if (!preg_match('/(host\d+)\.tokens\.db$/', $partnerDb, $m)) continue;
|
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) . ' '
|
. escapeshellarg($scope) . ' '
|
||||||
// Asked for per turn. Only meaningful on a profile holding web_search — the worker
|
// 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.
|
// 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 &';
|
. ' >/dev/null 2>&1 </dev/null &';
|
||||||
$out = []; $rc = 0;
|
$out = []; $rc = 0;
|
||||||
exec($cmd, $out, $rc);
|
exec($cmd, $out, $rc);
|
||||||
|
|||||||
Reference in New Issue
Block a user