xref: /plugin/aichat/Model/Generic/AbstractGenericModel.php (revision 7c3b69cbd638c7d1775b012bfb9b48d8136f76b9)
12e22aefbSAndreas Gohr<?php
22e22aefbSAndreas Gohr
32e22aefbSAndreas Gohrnamespace dokuwiki\plugin\aichat\Model\Generic;
42e22aefbSAndreas Gohr
52e22aefbSAndreas Gohruse dokuwiki\plugin\aichat\Model\AbstractModel;
62e22aefbSAndreas Gohruse dokuwiki\plugin\aichat\Model\ChatInterface;
72e22aefbSAndreas Gohruse dokuwiki\plugin\aichat\Model\EmbeddingInterface;
82e22aefbSAndreas Gohruse dokuwiki\plugin\aichat\Model\ModelException;
92e22aefbSAndreas Gohr
102e22aefbSAndreas Gohr/**
112e22aefbSAndreas Gohr * Abstract OpenAI-compatible Model
122e22aefbSAndreas Gohr *
132e22aefbSAndreas Gohr * This class provides a basic interface to the OpenAI API as implemented by many other providers.
142e22aefbSAndreas Gohr * It implements chat and embedding interfaces.
152e22aefbSAndreas Gohr */
162e22aefbSAndreas Gohrabstract class AbstractGenericModel extends AbstractModel implements ChatInterface, EmbeddingInterface
172e22aefbSAndreas Gohr{
182e22aefbSAndreas Gohr    /** @var string The API base URL */
192e22aefbSAndreas Gohr    protected $apiurl = '';
202e22aefbSAndreas Gohr
212e22aefbSAndreas Gohr
222e22aefbSAndreas Gohr    /** @inheritdoc */
232e22aefbSAndreas Gohr    public function __construct(string $name, array $config)
242e22aefbSAndreas Gohr    {
252e22aefbSAndreas Gohr        parent::__construct($name, $config);
262e22aefbSAndreas Gohr
272e22aefbSAndreas Gohr        if($this->apiurl === '') {
28*7c3b69cbSAndreas Gohr            $this->apiurl = $this->getFromConf('apiurl');
292e22aefbSAndreas Gohr        }
302e22aefbSAndreas Gohr        $this->apiurl = rtrim($this->apiurl, '/');
312e22aefbSAndreas Gohr    }
322e22aefbSAndreas Gohr
33*7c3b69cbSAndreas Gohr    /** @inheritdoc */
34*7c3b69cbSAndreas Gohr    protected function getHttpClient()
35*7c3b69cbSAndreas Gohr    {
36*7c3b69cbSAndreas Gohr        $http = parent::getHttpClient();
37*7c3b69cbSAndreas Gohr
38*7c3b69cbSAndreas Gohr        $apiKey = $this->getFromConf('apikey');
39*7c3b69cbSAndreas Gohr        $http->headers['Authorization'] = 'Bearer ' . $apiKey;
40*7c3b69cbSAndreas Gohr        return $http;
41*7c3b69cbSAndreas Gohr    }
42*7c3b69cbSAndreas Gohr
432e22aefbSAndreas Gohr    /**
442e22aefbSAndreas Gohr     * Send a request to the OpenAI API
452e22aefbSAndreas Gohr     *
462e22aefbSAndreas Gohr     * @param string $endpoint
472e22aefbSAndreas Gohr     * @param array $data Payload to send
482e22aefbSAndreas Gohr     * @return array API response
492e22aefbSAndreas Gohr     * @throws \Exception
502e22aefbSAndreas Gohr     */
512e22aefbSAndreas Gohr    protected function request($endpoint, $data)
522e22aefbSAndreas Gohr    {
532e22aefbSAndreas Gohr        $url = $this->apiurl . '/' . $endpoint;
542e22aefbSAndreas Gohr        return $this->sendAPIRequest('POST', $url, $data);
552e22aefbSAndreas Gohr    }
562e22aefbSAndreas Gohr
572e22aefbSAndreas Gohr    /** @inheritdoc */
582e22aefbSAndreas Gohr    protected function parseAPIResponse($response)
592e22aefbSAndreas Gohr    {
602e22aefbSAndreas Gohr        if (isset($response['usage'])) {
612e22aefbSAndreas Gohr            if(isset($response['usage']['prompt_tokens'])) {
622e22aefbSAndreas Gohr                $this->inputTokensUsed += $response['usage']['prompt_tokens'];
632e22aefbSAndreas Gohr            } elseif ($response['usage']['total_tokens']) {
642e22aefbSAndreas Gohr                // on embedding models, prompt_tokens is not available
652e22aefbSAndreas Gohr                $this->inputTokensUsed += $response['usage']['total_tokens'];
662e22aefbSAndreas Gohr            }
672e22aefbSAndreas Gohr            $this->outputTokensUsed += $response['usage']['completion_tokens'] ?? 0;
682e22aefbSAndreas Gohr        }
692e22aefbSAndreas Gohr
702e22aefbSAndreas Gohr        if (isset($response['error'])) {
712e22aefbSAndreas Gohr            throw new ModelException('API error: ' . $response['error']['message'], 3002);
722e22aefbSAndreas Gohr        }
732e22aefbSAndreas Gohr
742e22aefbSAndreas Gohr        return $response;
752e22aefbSAndreas Gohr    }
762e22aefbSAndreas Gohr
772e22aefbSAndreas Gohr    /** @inheritdoc */
782e22aefbSAndreas Gohr    public function getAnswer(array $messages): string
792e22aefbSAndreas Gohr    {
802e22aefbSAndreas Gohr        $data = [
812e22aefbSAndreas Gohr            'messages' => $messages,
822e22aefbSAndreas Gohr            'model' => $this->getModelName(),
832e22aefbSAndreas Gohr            'max_completion_tokens' => null,
842e22aefbSAndreas Gohr            'stream' => false,
852e22aefbSAndreas Gohr            'n' => 1, // number of completions
862e22aefbSAndreas Gohr            $data['temperature'] = 0.0
872e22aefbSAndreas Gohr        ];
882e22aefbSAndreas Gohr
892e22aefbSAndreas Gohr        $response = $this->request('chat/completions', $data);
902e22aefbSAndreas Gohr        return $response['choices'][0]['message']['content'];
912e22aefbSAndreas Gohr    }
922e22aefbSAndreas Gohr
932e22aefbSAndreas Gohr    /** @inheritdoc */
942e22aefbSAndreas Gohr    public function getEmbedding($text): array
952e22aefbSAndreas Gohr    {
962e22aefbSAndreas Gohr        $data = [
972e22aefbSAndreas Gohr            'model' => $this->getModelName(),
982e22aefbSAndreas Gohr            'input' => [$text],
992e22aefbSAndreas Gohr        ];
1002e22aefbSAndreas Gohr        $response = $this->request('embeddings', $data);
1012e22aefbSAndreas Gohr
1022e22aefbSAndreas Gohr        return $response['data'][0]['embedding'];
1032e22aefbSAndreas Gohr    }
1042e22aefbSAndreas Gohr
1052e22aefbSAndreas Gohr    /**
1062e22aefbSAndreas Gohr     * @internal for checking available models
1072e22aefbSAndreas Gohr     */
1082e22aefbSAndreas Gohr    public function listUpstreamModels()
1092e22aefbSAndreas Gohr    {
110*7c3b69cbSAndreas Gohr        $http = $this->getHttpClient();
1112e22aefbSAndreas Gohr        $url = $this->apiurl . '/models';
112*7c3b69cbSAndreas Gohr        return $http->get($url);
1132e22aefbSAndreas Gohr    }
1142e22aefbSAndreas Gohr}
115