xref: /plugin/aichat/Model/Generic/AbstractGenericModel.php (revision ac84f472b03ad860a0880f0cb3e86b0a20969598)
1<?php
2
3namespace dokuwiki\plugin\aichat\Model\Generic;
4
5use dokuwiki\plugin\aichat\Model\AbstractModel;
6use dokuwiki\plugin\aichat\Model\ChatInterface;
7use dokuwiki\plugin\aichat\Model\EmbeddingInterface;
8use dokuwiki\plugin\aichat\Model\ModelException;
9
10/**
11 * Abstract OpenAI-compatible Model
12 *
13 * This class provides a basic interface to the OpenAI API as implemented by many other providers.
14 * It implements chat and embedding interfaces.
15 */
16abstract class AbstractGenericModel extends AbstractModel implements ChatInterface, EmbeddingInterface
17{
18    /** @var string The API base URL */
19    protected $apiurl = '';
20
21
22    /** @inheritdoc */
23    public function __construct(string $name, array $config)
24    {
25        parent::__construct($name, $config);
26
27        if($this->apiurl === '') {
28            $this->apiurl = $this->getFromConf('apiurl');
29        }
30        $this->apiurl = rtrim($this->apiurl, '/');
31    }
32
33    /** @inheritdoc */
34    protected function getHttpClient()
35    {
36        $http = parent::getHttpClient();
37
38        $apiKey = $this->getFromConf('apikey');
39        $http->headers['Authorization'] = 'Bearer ' . $apiKey;
40        return $http;
41    }
42
43    /**
44     * Send a request to the OpenAI API
45     *
46     * @param string $endpoint
47     * @param array $data Payload to send
48     * @return array API response
49     * @throws \Exception
50     */
51    protected function request($endpoint, $data)
52    {
53        $url = $this->apiurl . '/' . $endpoint;
54        return $this->sendAPIRequest('POST', $url, $data);
55    }
56
57    /** @inheritdoc */
58    protected function parseAPIResponse($response)
59    {
60        if (isset($response['usage'])) {
61            if(isset($response['usage']['prompt_tokens'])) {
62                $this->inputTokensUsed += $response['usage']['prompt_tokens'];
63            } elseif ($response['usage']['total_tokens']) {
64                // on embedding models, prompt_tokens is not available
65                $this->inputTokensUsed += $response['usage']['total_tokens'];
66            }
67            $this->outputTokensUsed += $response['usage']['completion_tokens'] ?? 0;
68        }
69
70        if (isset($response['error'])) {
71            throw new ModelException('API error: ' . $response['error']['message'], 3002);
72        }
73
74        return $response;
75    }
76
77    /** @inheritdoc */
78    public function getAnswer(array $messages): string
79    {
80        $data = [
81            'messages' => $messages,
82            'model' => $this->getModelName(),
83            'max_completion_tokens' => null,
84            'stream' => false,
85            'n' => 1, // number of completions
86            'temperature' => 0.0
87        ];
88
89        $response = $this->request('chat/completions', $data);
90        return $response['choices'][0]['message']['content'];
91    }
92
93    /** @inheritdoc */
94    public function getEmbedding($text): array
95    {
96        $data = [
97            'model' => $this->getModelName(),
98            'input' => [$text],
99        ];
100        $response = $this->request('embeddings', $data);
101
102        return $response['data'][0]['embedding'];
103    }
104
105    /**
106     * @internal for checking available models
107     */
108    public function listUpstreamModels()
109    {
110        $http = $this->getHttpClient();
111        $url = $this->apiurl . '/models';
112        return $http->get($url);
113    }
114}
115