xref: /plugin/aichat/Model/AbstractModel.php (revision 4373d2bf7fcddc76e5ba367d903e3d0d86697dff)
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     * This method should check the response for any errors. If the API singalled an error,
45     * this method should throw an Exception with a meaningful error message.
46     *
47     * If the response returned any info on used tokens, they should be added to $this->tokensUsed
48     *
49     * The method should return the parsed response, which will be passed to the calling method.
50     *
51     * @param mixed $response the parsed JSON response from the API
52     * @return mixed
53     * @throws \Exception when the response indicates an error
54     */
55    abstract protected function parseAPIResponse($response);
56
57    /**
58     * Send a request to the API
59     *
60     * Model classes should use this method to send requests to the API.
61     *
62     * This method will take care of retrying and logging basic statistics.
63     *
64     * It is assumed that all APIs speak JSON.
65     *
66     * @param string $method The HTTP method to use (GET, POST, PUT, DELETE, etc.)
67     * @param string $url The full URL to send the request to
68     * @param array $data Payload to send, will be encoded to JSON
69     * @param int $retry How often this request has been retried, do not set externally
70     * @return array API response as returned by parseAPIResponse
71     * @throws \Exception when anything goes wrong
72     */
73    protected function sendAPIRequest($method, $url, $data, $retry = 0)
74    {
75        // init statistics
76        if ($retry === 0) {
77            $this->requestStart = microtime(true);
78        } else {
79            sleep($retry); // wait a bit between retries
80        }
81        $this->requestsMade++;
82
83        // encode payload data
84        try {
85            $json = json_encode($data, JSON_THROW_ON_ERROR);
86        } catch (\JsonException $e) {
87            $this->timeUsed += $this->requestStart - microtime(true);
88            throw new \Exception('Failed to encode JSON for API:' . $e->getMessage(), $e->getCode(), $e);
89        }
90
91        // send request and handle retries
92        $this->http->sendRequest($url, $json, $method);
93        $response = $this->http->resp_body;
94        if ($response === false || $this->http->error) {
95            if ($retry < self::MAX_RETRIES) {
96                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
97            }
98            $this->timeUsed += microtime(true) - $this->requestStart;
99            throw new \Exception('API returned no response. ' . $this->http->error);
100        }
101
102        // decode the response
103        try {
104            $result = json_decode((string)$response, true, 512, JSON_THROW_ON_ERROR);
105        } catch (\JsonException $e) {
106            $this->timeUsed += microtime(true) - $this->requestStart;
107            throw new \Exception('API returned invalid JSON: ' . $response, 0, $e);
108        }
109
110        // parse the response, retry on error
111        try {
112            $result = $this->parseAPIResponse($result);
113        } catch (\Exception $e) {
114            if ($retry < self::MAX_RETRIES) {
115                return $this->sendAPIRequest($method, $url, $data, $retry + 1);
116            }
117            $this->timeUsed += microtime(true) - $this->requestStart;
118            throw $e;
119        }
120
121        $this->timeUsed += microtime(true) - $this->requestStart;
122        return $result;
123    }
124
125    /**
126     * Reset the usage statistics
127     *
128     * Usually not needed when only handling one operation per request, but useful in CLI
129     */
130    public function resetUsageStats()
131    {
132        $this->tokensUsed = 0;
133        $this->timeUsed = 0;
134        $this->requestsMade = 0;
135    }
136
137    /**
138     * Get the usage statistics for this instance
139     *
140     * @return string[]
141     */
142    public function getUsageStats()
143    {
144        return [
145            'tokens' => $this->tokensUsed,
146            'cost' => round($this->tokensUsed * $this->get1kTokenPrice() / 1000, 4),
147            'time' => round($this->timeUsed, 2),
148            'requests' => $this->requestsMade,
149        ];
150    }
151}
152