microtime(true), 'model' => $modelId, 'name' => pretty_model_name($modelId), 'neurons' => round($neurons, 2), 'in' => $inTokens, 'out' => $outTokens, 'seconds' => round($seconds, 1), 'task' => $task, 'label' => $task !== '' ? task_label($task) : '', 'cached' => $cached, 'gateway' => Gateway::mode() !== 'direct', 'today' => round(UsageTracker::todayNeurons(), 0), 'budget' => UsageTracker::budgetEnabled() ? UsageTracker::budget() : UsageTracker::FREE_TIER, ]; self::$last = $entry; set_transient(self::key(get_current_user_id()), $entry, 10 * MINUTE_IN_SECONDS); } /** * Last request in this PHP process (for the log-context filter). * * @var array|null */ private static ?array $last = null; public static function logContext($context, $decoded = null, $logData = null): array { $context = is_array($context) ? $context : []; $usage = []; if (is_array($decoded)) { if (isset($decoded['usage']) && is_array($decoded['usage'])) { $usage = $decoded['usage']; } elseif (isset($decoded['result']['usage']) && is_array($decoded['result']['usage'])) { $usage = $decoded['result']['usage']; } } if ($usage !== [] || self::$last !== null) { $context['aipcf'] = [ 'neurons' => isset($usage['neurons']) ? round((float) $usage['neurons'], 3) : (self::$last['neurons'] ?? null), 'task' => \WordPress\CloudflareWorkersAiProvider\Routing\TaskContext::current(), 'gateway' => Gateway::mode(), ]; } return $context; } public static function registerRoutes(): void { register_rest_route('aipcf/v1', '/last', [ 'methods' => 'GET', 'permission_callback' => static function (): bool { return current_user_can('edit_posts'); }, 'callback' => static function (): \WP_REST_Response { $entry = get_transient(self::key(get_current_user_id())); return new \WP_REST_Response(is_array($entry) ? $entry : null, 200); }, ]); register_rest_route('aipcf/v1', '/usage', [ 'methods' => 'GET', 'permission_callback' => static function (): bool { return current_user_can('edit_posts'); }, 'callback' => static function (): \WP_REST_Response { $t = UsageTracker::todayTotals(); return new \WP_REST_Response([ 'today' => round((float) $t['neurons'], 0), 'budget' => UsageTracker::budgetEnabled() ? UsageTracker::budget() : UsageTracker::FREE_TIER, 'requests' => (int) $t['requests'], ], 200); }, ]); } public static function enqueueEditor(): void { if (!current_user_can('edit_posts')) { return; } wp_register_script('aipcf-receipts', false, ['wp-api-fetch', 'wp-data', 'wp-notices', 'wp-i18n'], AIPCF_VERSION, true); wp_enqueue_script('aipcf-receipts'); $i18n = [ 'guarded' => UsageTracker::budgetEnabled(), 'cached' => __('0 neurons (cached)', 'aipcf-ai-provider-for-cloudflare'), /* translators: %s: neurons */ 'neurons' => __('%s neurons', 'aipcf-ai-provider-for-cloudflare'), /* translators: Editor snackbar after an AI request; keep it short. 1: model name, 2: cost, such as "12 neurons" */ 'receipt' => __('%1$s: %2$s', 'aipcf-ai-provider-for-cloudflare'), /* translators: Editor snackbar after an AI request; keep it short. 1: model name, 2: cost, such as "12 neurons", 3: seconds the request took */ 'receiptTime' => __('%1$s: %2$s in %3$s s', 'aipcf-ai-provider-for-cloudflare'), /* translators: Editor snackbar after an AI request; keep it short. 1: task name, 2: model name, 3: cost, such as "12 neurons" */ 'receiptLabel' => __('%1$s (%2$s): %3$s', 'aipcf-ai-provider-for-cloudflare'), /* translators: Editor snackbar after an AI request; keep it short. 1: task name, 2: model name, 3: cost, such as "12 neurons", 4: seconds the request took */ 'receiptLabelTime' => __('%1$s (%2$s): %3$s in %4$s s', 'aipcf-ai-provider-for-cloudflare'), 'separator' => wp_get_list_item_separator(), /* translators: 1: neurons used today, 2: daily budget or free daily allowance, 3: number of short tasks */ 'leftOne' => __('%1$s / %2$s today, about %3$s short task left', 'aipcf-ai-provider-for-cloudflare'), /* translators: 1: neurons used today, 2: daily budget or free daily allowance, 3: number of short tasks */ 'leftMany' => __('%1$s / %2$s today, about %3$s short tasks left', 'aipcf-ai-provider-for-cloudflare'), /* translators: Admin bar counter; keep it short. "AIPCF" is the plugin's name; keep it. 1: neurons used today, 2: daily budget */ 'counter' => __('AIPCF %1$s / %2$s', 'aipcf-ai-provider-for-cloudflare'), /* translators: Admin bar counter when no daily budget is set; keep it short. "AIPCF" is the plugin's name; keep it. 1: neurons used today, 2: free daily allowance */ 'counterFree' => __('AIPCF %1$s / %2$s free', 'aipcf-ai-provider-for-cloudflare'), ]; wp_add_inline_script('aipcf-receipts', 'var aipcfReceipts = ' . wp_json_encode($i18n) . ';', 'before'); $js = <<<'JS' (function(){ if (!window.wp || !wp.apiFetch || !window.aipcfReceipts) { return; } var t = window.aipcfReceipts, seen = 0; function fmt(s, a){ var i = 0; return s.replace(/%(\d+)\$s|%s/g, function(m, n){ var v = n ? a[n - 1] : a[i++]; return String(v); }); } function isAiCall(path){ return typeof path === 'string' && path.indexOf('/aipcf/') === -1 && (/\/wp-abilities\/v1\/abilities\/[^?]+\/run/.test(path) || path.indexOf('/ai/v1/') !== -1); } function label(r){ var model = r.name || r.model, cost = r.cached ? t.cached : fmt(t.neurons, [r.neurons]), head; if (r.label) { head = r.seconds ? fmt(t.receiptLabelTime, [r.label, model, cost, r.seconds]) : fmt(t.receiptLabel, [r.label, model, cost]); } else { head = r.seconds ? fmt(t.receiptTime, [model, cost, r.seconds]) : fmt(t.receipt, [model, cost]); } var left = Math.max(0, r.budget - r.today), tasks = Math.floor(left / 2); return head + t.separator + fmt(tasks === 1 ? t.leftOne : t.leftMany, [r.today.toLocaleString(), r.budget.toLocaleString(), tasks.toLocaleString()]); } function show(){ wp.apiFetch({ path: '/aipcf/v1/last' }).then(function(r){ if (!r || r.time === seen) { return; } seen = r.time; if (wp.data && wp.data.dispatch('core/notices')) { wp.data.dispatch('core/notices').createNotice('info', label(r), { type: 'snackbar', isDismissible: true, id: 'aipcf-receipt' }); } var pill = document.querySelector('#wp-admin-bar-aipcf-usage .ab-item span'); if (pill) { pill.textContent = fmt(t.guarded ? t.counter : t.counterFree, [r.today.toLocaleString(), r.budget.toLocaleString()]); } }).catch(function(){}); } wp.apiFetch.use(function(options, next){ var p = next(options); if (isAiCall(options.path || options.url)) { p.then(function(){ setTimeout(show, 200); }, function(){ setTimeout(show, 200); }); } return p; }); })(); JS; wp_add_inline_script('aipcf-receipts', $js); } }