1f6ef2e50SAndreas Gohr<?php 2f6ef2e50SAndreas Gohr 3f6ef2e50SAndreas Gohrnamespace dokuwiki\plugin\aichat\Model; 4f6ef2e50SAndreas Gohr 5294a9eafSAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient; 6294a9eafSAndreas Gohr 7294a9eafSAndreas Gohr/** 8294a9eafSAndreas Gohr * Base class for all models 9294a9eafSAndreas Gohr * 10294a9eafSAndreas Gohr * Model classes also need to implement one of the following interfaces: 11294a9eafSAndreas Gohr * - ChatInterface 12294a9eafSAndreas Gohr * - EmbeddingInterface 13294a9eafSAndreas Gohr */ 147ebc7895Ssplitbrainabstract class AbstractModel 157ebc7895Ssplitbrain{ 16*34a1c478SAndreas Gohr /** @var bool debug API communication */ 17*34a1c478SAndreas Gohr protected $debug = false; 18*34a1c478SAndreas Gohr 19*34a1c478SAndreas Gohr 20*34a1c478SAndreas Gohr protected $inputTokensUsed = 0; 21*34a1c478SAndreas Gohr protected $outputTokensUsed = 0; 22f6ef2e50SAndreas Gohr protected $tokensUsed = 0; 23*34a1c478SAndreas Gohr 24f6ef2e50SAndreas Gohr /** @var int total time spent in requests by this instance */ 25f6ef2e50SAndreas Gohr protected $timeUsed = 0; 26f6ef2e50SAndreas Gohr /** @var int total number of requests made by this instance */ 27f6ef2e50SAndreas Gohr protected $requestsMade = 0; 28294a9eafSAndreas Gohr /** @var int How often to retry a request if it fails */ 29294a9eafSAndreas Gohr public const MAX_RETRIES = 3; 30294a9eafSAndreas Gohr /** @var DokuHTTPClient */ 31294a9eafSAndreas Gohr protected $http; 32294a9eafSAndreas Gohr /** @var int start time of the current request chain (may be multiple when retries needed) */ 33294a9eafSAndreas Gohr protected $requestStart = 0; 34f6ef2e50SAndreas Gohr 35f6ef2e50SAndreas Gohr /** 36294a9eafSAndreas Gohr * This initializes a HTTP client 37294a9eafSAndreas Gohr * 38294a9eafSAndreas Gohr * Implementors should override this and authenticate the client. 39294a9eafSAndreas Gohr * 40294a9eafSAndreas Gohr * @param array $config The plugin configuration 41f6ef2e50SAndreas Gohr */ 42d02b7935SAndreas Gohr public function __construct(array $config) 43294a9eafSAndreas Gohr { 44294a9eafSAndreas Gohr $this->http = new DokuHTTPClient(); 45294a9eafSAndreas Gohr $this->http->timeout = 60; 46294a9eafSAndreas Gohr $this->http->headers['Content-Type'] = 'application/json'; 47294a9eafSAndreas Gohr } 48f6ef2e50SAndreas Gohr 49f6ef2e50SAndreas Gohr /** 50*34a1c478SAndreas Gohr * When enabled, the input/output of the API will be printed to STDOUT 51*34a1c478SAndreas Gohr * 52*34a1c478SAndreas Gohr * @param bool $debug 53*34a1c478SAndreas Gohr */ 54*34a1c478SAndreas Gohr public function setDebug($debug = true) 55*34a1c478SAndreas Gohr { 56*34a1c478SAndreas Gohr $this->debug = $debug; 57*34a1c478SAndreas Gohr } 58*34a1c478SAndreas Gohr 59*34a1c478SAndreas Gohr /** 60294a9eafSAndreas Gohr * This method should check the response for any errors. If the API singalled an error, 61294a9eafSAndreas Gohr * this method should throw an Exception with a meaningful error message. 62294a9eafSAndreas Gohr * 63294a9eafSAndreas Gohr * If the response returned any info on used tokens, they should be added to $this->tokensUsed 64294a9eafSAndreas Gohr * 65294a9eafSAndreas Gohr * The method should return the parsed response, which will be passed to the calling method. 66294a9eafSAndreas Gohr * 67294a9eafSAndreas Gohr * @param mixed $response the parsed JSON response from the API 68294a9eafSAndreas Gohr * @return mixed 69294a9eafSAndreas Gohr * @throws \Exception when the response indicates an error 70294a9eafSAndreas Gohr */ 71294a9eafSAndreas Gohr abstract protected function parseAPIResponse($response); 72294a9eafSAndreas Gohr 73294a9eafSAndreas Gohr /** 74294a9eafSAndreas Gohr * Send a request to the API 75294a9eafSAndreas Gohr * 76294a9eafSAndreas Gohr * Model classes should use this method to send requests to the API. 77294a9eafSAndreas Gohr * 78294a9eafSAndreas Gohr * This method will take care of retrying and logging basic statistics. 79294a9eafSAndreas Gohr * 80294a9eafSAndreas Gohr * It is assumed that all APIs speak JSON. 81294a9eafSAndreas Gohr * 82294a9eafSAndreas Gohr * @param string $method The HTTP method to use (GET, POST, PUT, DELETE, etc.) 83294a9eafSAndreas Gohr * @param string $url The full URL to send the request to 84294a9eafSAndreas Gohr * @param array $data Payload to send, will be encoded to JSON 85294a9eafSAndreas Gohr * @param int $retry How often this request has been retried, do not set externally 86294a9eafSAndreas Gohr * @return array API response as returned by parseAPIResponse 87294a9eafSAndreas Gohr * @throws \Exception when anything goes wrong 88294a9eafSAndreas Gohr */ 89294a9eafSAndreas Gohr protected function sendAPIRequest($method, $url, $data, $retry = 0) 90294a9eafSAndreas Gohr { 91294a9eafSAndreas Gohr // init statistics 92294a9eafSAndreas Gohr if ($retry === 0) { 93294a9eafSAndreas Gohr $this->requestStart = microtime(true); 94294a9eafSAndreas Gohr } else { 95294a9eafSAndreas Gohr sleep($retry); // wait a bit between retries 96294a9eafSAndreas Gohr } 97294a9eafSAndreas Gohr $this->requestsMade++; 98294a9eafSAndreas Gohr 99294a9eafSAndreas Gohr // encode payload data 100294a9eafSAndreas Gohr try { 101*34a1c478SAndreas Gohr $json = json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); 102294a9eafSAndreas Gohr } catch (\JsonException $e) { 103294a9eafSAndreas Gohr $this->timeUsed += $this->requestStart - microtime(true); 104294a9eafSAndreas Gohr throw new \Exception('Failed to encode JSON for API:' . $e->getMessage(), $e->getCode(), $e); 105294a9eafSAndreas Gohr } 106294a9eafSAndreas Gohr 107*34a1c478SAndreas Gohr if ($this->debug) { 108*34a1c478SAndreas Gohr echo 'Sending ' . $method . ' request to ' . $url . ' with payload:' . "\n"; 109*34a1c478SAndreas Gohr print_r($json); 110*34a1c478SAndreas Gohr } 111*34a1c478SAndreas Gohr 112294a9eafSAndreas Gohr // send request and handle retries 113294a9eafSAndreas Gohr $this->http->sendRequest($url, $json, $method); 114294a9eafSAndreas Gohr $response = $this->http->resp_body; 115294a9eafSAndreas Gohr if ($response === false || $this->http->error) { 116294a9eafSAndreas Gohr if ($retry < self::MAX_RETRIES) { 117294a9eafSAndreas Gohr return $this->sendAPIRequest($method, $url, $data, $retry + 1); 118294a9eafSAndreas Gohr } 119294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 120294a9eafSAndreas Gohr throw new \Exception('API returned no response. ' . $this->http->error); 121294a9eafSAndreas Gohr } 122294a9eafSAndreas Gohr 123*34a1c478SAndreas Gohr if ($this->debug) { 124*34a1c478SAndreas Gohr echo 'Received response:' . "\n"; 125*34a1c478SAndreas Gohr print_r($response); 126*34a1c478SAndreas Gohr } 127*34a1c478SAndreas Gohr 128294a9eafSAndreas Gohr // decode the response 129294a9eafSAndreas Gohr try { 130294a9eafSAndreas Gohr $result = json_decode((string)$response, true, 512, JSON_THROW_ON_ERROR); 131294a9eafSAndreas Gohr } catch (\JsonException $e) { 132294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 133294a9eafSAndreas Gohr throw new \Exception('API returned invalid JSON: ' . $response, 0, $e); 134294a9eafSAndreas Gohr } 135294a9eafSAndreas Gohr 136294a9eafSAndreas Gohr // parse the response, retry on error 137294a9eafSAndreas Gohr try { 138294a9eafSAndreas Gohr $result = $this->parseAPIResponse($result); 139294a9eafSAndreas Gohr } catch (\Exception $e) { 140294a9eafSAndreas Gohr if ($retry < self::MAX_RETRIES) { 141294a9eafSAndreas Gohr return $this->sendAPIRequest($method, $url, $data, $retry + 1); 142294a9eafSAndreas Gohr } 143294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 144294a9eafSAndreas Gohr throw $e; 145294a9eafSAndreas Gohr } 146294a9eafSAndreas Gohr 147294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 148294a9eafSAndreas Gohr return $result; 149294a9eafSAndreas Gohr } 150294a9eafSAndreas Gohr 151f6ef2e50SAndreas Gohr /** 152f6ef2e50SAndreas Gohr * Reset the usage statistics 153f6ef2e50SAndreas Gohr * 154f6ef2e50SAndreas Gohr * Usually not needed when only handling one operation per request, but useful in CLI 155f6ef2e50SAndreas Gohr */ 156f6ef2e50SAndreas Gohr public function resetUsageStats() 157f6ef2e50SAndreas Gohr { 158f6ef2e50SAndreas Gohr $this->tokensUsed = 0; 159f6ef2e50SAndreas Gohr $this->timeUsed = 0; 160f6ef2e50SAndreas Gohr $this->requestsMade = 0; 161f6ef2e50SAndreas Gohr } 162f6ef2e50SAndreas Gohr 163f6ef2e50SAndreas Gohr /** 164f6ef2e50SAndreas Gohr * Get the usage statistics for this instance 165f6ef2e50SAndreas Gohr * 166f6ef2e50SAndreas Gohr * @return string[] 167f6ef2e50SAndreas Gohr */ 168f6ef2e50SAndreas Gohr public function getUsageStats() 169f6ef2e50SAndreas Gohr { 170*34a1c478SAndreas Gohr 171*34a1c478SAndreas Gohr $cost = 0; 172*34a1c478SAndreas Gohr $cost += $this->inputTokensUsed * $this->getInputTokenPrice(); 173*34a1c478SAndreas Gohr if ($this instanceof ChatInterface) { 174*34a1c478SAndreas Gohr $cost += $this->outputTokensUsed * $this->getOutputTokenPrice(); 175*34a1c478SAndreas Gohr } 176*34a1c478SAndreas Gohr 177f6ef2e50SAndreas Gohr return [ 178*34a1c478SAndreas Gohr 'tokens' => $this->tokensUsed + $this->inputTokensUsed + $this->outputTokensUsed, 179*34a1c478SAndreas Gohr 'cost' => round($cost / 1_000_000, 4), 180f6ef2e50SAndreas Gohr 'time' => round($this->timeUsed, 2), 181f6ef2e50SAndreas Gohr 'requests' => $this->requestsMade, 182f6ef2e50SAndreas Gohr ]; 183f6ef2e50SAndreas Gohr } 184f6ef2e50SAndreas Gohr} 185