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