1f6ef2e50SAndreas Gohr<?php 2f6ef2e50SAndreas Gohr 3f6ef2e50SAndreas Gohrnamespace dokuwiki\plugin\aichat\Model; 4f6ef2e50SAndreas Gohr 5*294a9eafSAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient; 6*294a9eafSAndreas Gohr 7*294a9eafSAndreas Gohr/** 8*294a9eafSAndreas Gohr * Base class for all models 9*294a9eafSAndreas Gohr * 10*294a9eafSAndreas Gohr * Model classes also need to implement one of the following interfaces: 11*294a9eafSAndreas Gohr * - ChatInterface 12*294a9eafSAndreas Gohr * - EmbeddingInterface 13*294a9eafSAndreas Gohr */ 147ebc7895Ssplitbrainabstract class AbstractModel 157ebc7895Ssplitbrain{ 16f6ef2e50SAndreas Gohr /** @var int total tokens used by this instance */ 17f6ef2e50SAndreas Gohr protected $tokensUsed = 0; 18f6ef2e50SAndreas Gohr /** @var int total time spent in requests by this instance */ 19f6ef2e50SAndreas Gohr protected $timeUsed = 0; 20f6ef2e50SAndreas Gohr /** @var int total number of requests made by this instance */ 21f6ef2e50SAndreas Gohr protected $requestsMade = 0; 22*294a9eafSAndreas Gohr /** @var int How often to retry a request if it fails */ 23*294a9eafSAndreas Gohr public const MAX_RETRIES = 3; 24*294a9eafSAndreas Gohr /** @var DokuHTTPClient */ 25*294a9eafSAndreas Gohr protected $http; 26*294a9eafSAndreas Gohr /** @var int start time of the current request chain (may be multiple when retries needed) */ 27*294a9eafSAndreas Gohr protected $requestStart = 0; 28f6ef2e50SAndreas Gohr 29f6ef2e50SAndreas Gohr /** 30*294a9eafSAndreas Gohr * This initializes a HTTP client 31*294a9eafSAndreas Gohr * 32*294a9eafSAndreas Gohr * Implementors should override this and authenticate the client. 33*294a9eafSAndreas Gohr * 34*294a9eafSAndreas Gohr * @param array $config The plugin configuration 35f6ef2e50SAndreas Gohr */ 36*294a9eafSAndreas Gohr public function __construct() 37*294a9eafSAndreas Gohr { 38*294a9eafSAndreas Gohr $this->http = new DokuHTTPClient(); 39*294a9eafSAndreas Gohr $this->http->timeout = 60; 40*294a9eafSAndreas Gohr $this->http->headers['Content-Type'] = 'application/json'; 41*294a9eafSAndreas Gohr } 42f6ef2e50SAndreas Gohr 43f6ef2e50SAndreas Gohr /** 446a18e0f4SAndreas Gohr * The name as used by the LLM provider 45f6ef2e50SAndreas Gohr * 466a18e0f4SAndreas Gohr * @return string 47f6ef2e50SAndreas Gohr */ 486a18e0f4SAndreas Gohr abstract public function getModelName(); 49f6ef2e50SAndreas Gohr 50f6ef2e50SAndreas Gohr /** 516a18e0f4SAndreas Gohr * Get the price for 1000 tokens 52f6ef2e50SAndreas Gohr * 536a18e0f4SAndreas Gohr * @return float 54f6ef2e50SAndreas Gohr */ 556a18e0f4SAndreas Gohr abstract public function get1kTokenPrice(); 56f6ef2e50SAndreas Gohr 57*294a9eafSAndreas Gohr 58*294a9eafSAndreas Gohr /** 59*294a9eafSAndreas Gohr * This method should check the response for any errors. If the API singalled an error, 60*294a9eafSAndreas Gohr * this method should throw an Exception with a meaningful error message. 61*294a9eafSAndreas Gohr * 62*294a9eafSAndreas Gohr * If the response returned any info on used tokens, they should be added to $this->tokensUsed 63*294a9eafSAndreas Gohr * 64*294a9eafSAndreas Gohr * The method should return the parsed response, which will be passed to the calling method. 65*294a9eafSAndreas Gohr * 66*294a9eafSAndreas Gohr * @param mixed $response the parsed JSON response from the API 67*294a9eafSAndreas Gohr * @return mixed 68*294a9eafSAndreas Gohr * @throws \Exception when the response indicates an error 69*294a9eafSAndreas Gohr */ 70*294a9eafSAndreas Gohr abstract protected function parseAPIResponse($response); 71*294a9eafSAndreas Gohr 72*294a9eafSAndreas Gohr /** 73*294a9eafSAndreas Gohr * Send a request to the API 74*294a9eafSAndreas Gohr * 75*294a9eafSAndreas Gohr * Model classes should use this method to send requests to the API. 76*294a9eafSAndreas Gohr * 77*294a9eafSAndreas Gohr * This method will take care of retrying and logging basic statistics. 78*294a9eafSAndreas Gohr * 79*294a9eafSAndreas Gohr * It is assumed that all APIs speak JSON. 80*294a9eafSAndreas Gohr * 81*294a9eafSAndreas Gohr * @param string $method The HTTP method to use (GET, POST, PUT, DELETE, etc.) 82*294a9eafSAndreas Gohr * @param string $url The full URL to send the request to 83*294a9eafSAndreas Gohr * @param array $data Payload to send, will be encoded to JSON 84*294a9eafSAndreas Gohr * @param int $retry How often this request has been retried, do not set externally 85*294a9eafSAndreas Gohr * @return array API response as returned by parseAPIResponse 86*294a9eafSAndreas Gohr * @throws \Exception when anything goes wrong 87*294a9eafSAndreas Gohr */ 88*294a9eafSAndreas Gohr protected function sendAPIRequest($method, $url, $data, $retry = 0) 89*294a9eafSAndreas Gohr { 90*294a9eafSAndreas Gohr // init statistics 91*294a9eafSAndreas Gohr if ($retry === 0) { 92*294a9eafSAndreas Gohr $this->requestStart = microtime(true); 93*294a9eafSAndreas Gohr } else { 94*294a9eafSAndreas Gohr sleep($retry); // wait a bit between retries 95*294a9eafSAndreas Gohr } 96*294a9eafSAndreas Gohr $this->requestsMade++; 97*294a9eafSAndreas Gohr 98*294a9eafSAndreas Gohr // encode payload data 99*294a9eafSAndreas Gohr try { 100*294a9eafSAndreas Gohr $json = json_encode($data, JSON_THROW_ON_ERROR); 101*294a9eafSAndreas Gohr } catch (\JsonException $e) { 102*294a9eafSAndreas Gohr $this->timeUsed += $this->requestStart - microtime(true); 103*294a9eafSAndreas Gohr throw new \Exception('Failed to encode JSON for API:' . $e->getMessage(), $e->getCode(), $e); 104*294a9eafSAndreas Gohr } 105*294a9eafSAndreas Gohr 106*294a9eafSAndreas Gohr // send request and handle retries 107*294a9eafSAndreas Gohr $this->http->sendRequest($url, $json, $method); 108*294a9eafSAndreas Gohr $response = $this->http->resp_body; 109*294a9eafSAndreas Gohr if ($response === false || $this->http->error) { 110*294a9eafSAndreas Gohr if ($retry < self::MAX_RETRIES) { 111*294a9eafSAndreas Gohr return $this->sendAPIRequest($method, $url, $data, $retry + 1); 112*294a9eafSAndreas Gohr } 113*294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 114*294a9eafSAndreas Gohr throw new \Exception('API returned no response. ' . $this->http->error); 115*294a9eafSAndreas Gohr } 116*294a9eafSAndreas Gohr 117*294a9eafSAndreas Gohr // decode the response 118*294a9eafSAndreas Gohr try { 119*294a9eafSAndreas Gohr $result = json_decode((string)$response, true, 512, JSON_THROW_ON_ERROR); 120*294a9eafSAndreas Gohr } catch (\JsonException $e) { 121*294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 122*294a9eafSAndreas Gohr throw new \Exception('API returned invalid JSON: ' . $response, 0, $e); 123*294a9eafSAndreas Gohr } 124*294a9eafSAndreas Gohr 125*294a9eafSAndreas Gohr // parse the response, retry on error 126*294a9eafSAndreas Gohr try { 127*294a9eafSAndreas Gohr $result = $this->parseAPIResponse($result); 128*294a9eafSAndreas Gohr } catch (\Exception $e) { 129*294a9eafSAndreas Gohr if ($retry < self::MAX_RETRIES) { 130*294a9eafSAndreas Gohr return $this->sendAPIRequest($method, $url, $data, $retry + 1); 131*294a9eafSAndreas Gohr } 132*294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 133*294a9eafSAndreas Gohr throw $e; 134*294a9eafSAndreas Gohr } 135*294a9eafSAndreas Gohr 136*294a9eafSAndreas Gohr $this->timeUsed += microtime(true) - $this->requestStart; 137*294a9eafSAndreas Gohr return $result; 138*294a9eafSAndreas Gohr } 139*294a9eafSAndreas Gohr 140f6ef2e50SAndreas Gohr /** 141f6ef2e50SAndreas Gohr * Reset the usage statistics 142f6ef2e50SAndreas Gohr * 143f6ef2e50SAndreas Gohr * Usually not needed when only handling one operation per request, but useful in CLI 144f6ef2e50SAndreas Gohr */ 145f6ef2e50SAndreas Gohr public function resetUsageStats() 146f6ef2e50SAndreas Gohr { 147f6ef2e50SAndreas Gohr $this->tokensUsed = 0; 148f6ef2e50SAndreas Gohr $this->timeUsed = 0; 149f6ef2e50SAndreas Gohr $this->requestsMade = 0; 150f6ef2e50SAndreas Gohr } 151f6ef2e50SAndreas Gohr 152f6ef2e50SAndreas Gohr /** 153f6ef2e50SAndreas Gohr * Get the usage statistics for this instance 154f6ef2e50SAndreas Gohr * 155f6ef2e50SAndreas Gohr * @return string[] 156f6ef2e50SAndreas Gohr */ 157f6ef2e50SAndreas Gohr public function getUsageStats() 158f6ef2e50SAndreas Gohr { 159f6ef2e50SAndreas Gohr return [ 160f6ef2e50SAndreas Gohr 'tokens' => $this->tokensUsed, 161*294a9eafSAndreas Gohr 'cost' => round($this->tokensUsed * $this->get1kTokenPrice() / 1000, 4), // FIXME handle float precision 162f6ef2e50SAndreas Gohr 'time' => round($this->timeUsed, 2), 163f6ef2e50SAndreas Gohr 'requests' => $this->requestsMade, 164f6ef2e50SAndreas Gohr ]; 165f6ef2e50SAndreas Gohr } 166f6ef2e50SAndreas Gohr} 167