* @author Maximum Software Inc * @license GPL-3.0-only * @link https://agenistrator.com/ * * SPDX-FileCopyrightText: 2025-2026 Alex Ruban * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc * SPDX-License-Identifier: GPL-3.0-only */ namespace Agenistrator; use guttedgarden\Tiktoken\EncoderProvider; use guttedgarden\Tiktoken\Encoder; // Direct access prevention if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Token_Counter class. */ class Token_Counter { use Singleton; /** * Encoding to use for all providers */ private const ENCODING = 'cl100k_base'; /** * Characters per token for approximation fallback * Conservative average based on Anthropic (~4) and AWS Bedrock (~6) documentation */ private const CHARS_PER_TOKEN = 4; /** * Encoder instance (lazy loaded) * @var Encoder|null */ private $encoder = null; /** * Whether tiktoken initialization failed and we're using fallback * @var bool */ private $using_fallback = false; /** * Get the encoder instance (lazy loaded) * Returns null if tiktoken fails to initialize * * @return Encoder|null */ private function get_encoder() { if ( $this->using_fallback ) { return null; } if ( null === $this->encoder ) { try { $provider = new EncoderProvider(); $cache_dir = WP_CONTENT_DIR . '/cache/agenistrator/tiktoken'; if ( ! is_dir( $cache_dir ) ) { wp_mkdir_p( $cache_dir ); } $provider->setVocabCache( $cache_dir ); $this->encoder = $provider->get( self::ENCODING ); } catch ( \Throwable $e ) { $this->using_fallback = true; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Production error logging for tiktoken initialization failures error_log( String_Utils::replace_tags( __( 'Agenistrator: tiktoken initialization failed, using approximation fallback: {error}', 'agenistrator' ), array( 'error' => $e->getMessage() ) ) ); return null; } } return $this->encoder; } /** * Count tokens in a text string * * @param string $text Text to count tokens for * @return int Token count * @since 1.0.0 */ public function count_tokens( $text ) { if ( empty( $text ) ) { return 0; } $encoder = $this->get_encoder(); if ( null !== $encoder ) { return count( $encoder->encode( $text ) ); } return $this->approximate_tokens( $text ); } /** * Approximate token count using character-based heuristic * Used as fallback when tiktoken library fails to load * * @param string $text Text to approximate tokens for * @return int Approximate token count */ private function approximate_tokens( $text ) { return (int) ceil( String_Utils::mb_strlen( $text ) / self::CHARS_PER_TOKEN ); } /** * Get encoding identifier used for token counting * * @return string Encoding name or 'approximation' if using fallback * @since 1.0.0 */ public function get_encoding() { return $this->get_encoder() === null ? 'approximation' : self::ENCODING; } /** * Check if using fallback approximation tokenizer * * @return bool True if tiktoken failed and we're using approximation * @since 1.0.0 */ public function is_using_fallback() { return $this->using_fallback; } }