30 public $lastRequest =
"";
33 public $lastResponse =
"";
36 public $lastUsage = array();
62 public function __construct(
string $type,
string $key,
string $baseUrl,
string $model,
int $timeout)
64 $this->
type = strtolower($type);
66 $this->baseUrl = rtrim($baseUrl,
'/');
67 $this->model = $model;
68 $this->timeout = $timeout;
85 public function generate(
string $system,
string $userMsg,
string $mode =
'text', array $attachments = array(), array $history = array()): ?string
87 switch ($this->
type) {
89 return $this->
callAnthropic($system, $userMsg, $mode, $attachments, $history);
91 return $this->
callGoogle($system, $userMsg, $mode, $attachments, $history);
93 return $this->
callOpenAI($system, $userMsg, $mode, $attachments, $history);
107 private function callOpenAI(
string $sys,
string $msg,
string $mode =
'text', array $attachments = array(), array $history = array()): ?string
109 $url = $this->baseUrl;
110 if (strpos($url,
'/chat/completions') ===
false && strpos($url,
'/generate') ===
false) {
111 $url .=
'/chat/completions';
117 if (!empty($attachments)) {
118 $userContent = array(array(
"type" =>
"text",
"text" => $msg));
119 foreach ($attachments as $att) {
120 if ($att[
'mime'] ===
'application/pdf') {
123 $userContent[] = array(
126 "filename" =>
"document.pdf",
127 "file_data" =>
"data:application/pdf;base64,".$att[
'data']
131 $userContent[] = array(
132 "type" =>
"image_url",
133 "image_url" => array(
"url" =>
"data:".$att[
'mime'].
";base64,".$att[
'data'])
139 $messages = array(array(
"role" =>
"system",
"content" => $sys));
140 foreach ($history as $turn) {
141 $messages[] = array(
"role" => (($turn[
'role'] ??
'') ===
'assistant' ?
'assistant' :
'user'),
"content" => (
string) $turn[
'text']);
143 $messages[] = array(
"role" =>
"user",
"content" => $userContent);
146 "model" => $this->model,
147 "messages" => $messages,
150 if (!empty($attachments)) {
151 $data[
"max_tokens"] = 8192;
156 if ($mode ===
'json') {
158 if (strpos($url,
'openai') !==
false || strpos($url,
'deepseek') !==
false || strpos($url,
'perplexity') !==
false || strpos($url,
'mistral') !==
false || strpos($url,
'zai') !==
false) {
159 $data[
"response_format"] = array(
"type" =>
"json_object");
165 return $this->
curl($url, $data, array(
"Content-Type: application/json",
"Authorization: Bearer " . $this->key));
179 private function callAnthropic(
string $sys,
string $msg,
string $mode =
'text', array $attachments = array(), array $history = array())
182 $url = $this->baseUrl . (strpos($this->baseUrl,
'/messages') ===
false ?
'/messages' :
'');
188 if (!empty($attachments)) {
189 $userContent = array();
190 foreach ($attachments as $att) {
191 $userContent[] = array(
192 "type" => ($att[
'mime'] ===
'application/pdf' ?
"document" :
"image"),
195 "source" => array(
"type" =>
"base64",
"media_type" => $att[
'mime'],
"data" => $att[
'data'])
198 $userContent[] = array(
"type" =>
"text",
"text" => $msg);
203 foreach ($history as $turn) {
204 $messages[] = array(
"role" => (($turn[
'role'] ??
'') ===
'assistant' ?
'assistant' :
'user'),
"content" => (
string) $turn[
'text']);
206 $messages[] = array(
"role" =>
"user",
"content" => $userContent);
209 "model" => $this->model,
211 "messages" => $messages,
212 "max_tokens" => $maxTokens
217 return $this->
curl($url, $data, array(
"content-type: application/json",
"x-api-key: " . $this->key,
"anthropic-version: 2023-06-01"),
true);
231 private function callGoogle(
string $sys,
string $msg,
string $mode =
'text', array $attachments = array(), array $history = array())
233 $url = $this->baseUrl;
236 if (strpos($url,
':generateContent') ===
false) {
237 if (strpos($url,
'/models/') ===
false) {
238 $url .=
"/models/" . $this->model;
240 $url .=
":generateContent";
243 $url .=
"?key=" . $this->key;
248 foreach ($attachments as $att) {
249 $parts[] = array(
"inline_data" => array(
"mime_type" => $att[
'mime'],
"data" => $att[
'data']));
251 $parts[] = array(
"text" => $sys .
"\nUser: " . $msg);
258 if (!empty($history)) {
259 foreach ($history as $turn) {
261 "role" => (($turn[
'role'] ??
'') ===
'assistant' ?
'model' :
'user'),
262 "parts" => array(array(
"text" => (
string) $turn[
'text']))
265 $contents[] = array(
"role" =>
"user",
"parts" => $parts);
267 $contents[] = array(
"parts" => $parts);
271 "contents" => $contents,
272 "generationConfig" => (empty($attachments) ? array(
"temperature" => 0.1) : array(
"temperature" => 0.1,
"maxOutputTokens" => 16384))
277 return $this->
curl($url, $data, array(
"Content-Type: application/json"),
false,
true);
289 if (!is_object($db) || !is_object(
$conf)) {
296 $rec = json_decode($raw,
true);
297 if (!is_array($rec) || (
string) ($rec[
'model'] ??
'') !== (
string) $this->model) {
300 include_once DOL_DOCUMENT_ROOT.
'/core/lib/admin.lib.php';
319 if (!is_object($db) || !is_object(
$conf)) {
323 if (!preg_match(
'/model/i', $msg)) {
326 if (!preg_match(
'/not.?found|does not exist|not exist|unsupported|not supported|not available|unavailable|deprecated|no longer|retired|invalid/i', $msg)) {
329 include_once DOL_DOCUMENT_ROOT.
'/core/lib/admin.lib.php';
334 'model' => $this->model,
336 'http_code' => $httpCode,
338 )),
'chaine', 0,
'',
$conf->entity);
352 $json = json_encode($data);
356 $out = preg_replace_callback(
357 '~((?:[A-Za-z0-9+=]++|\\\\/)+)~',
362 static function (array $m) {
363 if (strlen($m[1]) < 512) {
367 return '[base64 elided, '.strlen($m[1]).
' chars]';
372 return ($out ===
null) ? $json : $out;
385 private function curl(
string $url, array $data, array $headers,
bool $isClaude =
false,
bool $isGemini =
false): ?string
387 include_once DOL_DOCUMENT_ROOT.
'/core/lib/geturl.lib.php';
391 global $dolibarr_ai_allow_local_endpoints;
393 $localurl = empty($dolibarr_ai_allow_local_endpoints) ? 0 : 2;
398 $this->lastUsage = array();
400 $result =
getURLContent($url,
'POST', json_encode($data), 1, $headers, array(
'http',
'https'), $localurl, -1, 0, $this->timeout);
402 $body = (string) ($result[
'content'] ??
'');
403 $httpCode = (int) ($result[
'http_code'] ?? 0);
404 $effectiveUrl = (string) ($result[
'url'] ?? $url);
408 $effectiveUrl = preg_replace(
'/([?&]key=)[^&\s]+/',
'$1***', $effectiveUrl);
412 $this->lastResponse =
"HTTP " . $httpCode .
" from " . $effectiveUrl .
"\n--- body (" . strlen($body) .
" bytes) ---\n" . $body;
414 if (!empty($result[
'curl_error_no'])) {
415 return "Error: cURL #" . $result[
'curl_error_no'] .
" " . $result[
'curl_error_msg'] .
" (url=" . $effectiveUrl .
")";
418 $json = json_decode($body,
true);
420 if ($json ===
null && json_last_error() !== JSON_ERROR_NONE) {
424 $snippet = substr($body, 0, 500);
425 return "Error: Invalid JSON response from API (HTTP " . $httpCode .
", " . strlen($body) .
" bytes). Body snippet: " . ($snippet !==
'' ? $snippet :
'<empty>');
428 if (isset($json[
'error'])) {
429 $msg = $json[
'error'][
'message'] ?? json_encode($json[
'error']);
431 return "Error: API " . $msg;
441 $this->lastUsage = array(
'model' => $this->model);
443 $this->lastUsage[
'input'] = (int) ($json[
'usageMetadata'][
'promptTokenCount'] ?? 0);
444 $this->lastUsage[
'output'] = (int) ($json[
'usageMetadata'][
'candidatesTokenCount'] ?? 0) + (int) ($json[
'usageMetadata'][
'thoughtsTokenCount'] ?? 0);
445 } elseif ($isClaude) {
446 $this->lastUsage[
'input'] = (int) ($json[
'usage'][
'input_tokens'] ?? 0);
447 $this->lastUsage[
'output'] = (int) ($json[
'usage'][
'output_tokens'] ?? 0);
449 $this->lastUsage[
'input'] = (int) ($json[
'usage'][
'prompt_tokens'] ?? 0);
450 $this->lastUsage[
'output'] = (int) ($json[
'usage'][
'completion_tokens'] ?? 0);
455 return $json[
'content'][0][
'text'] ??
null;
458 return $json[
'candidates'][0][
'content'][
'parts'][0][
'text'] ??
null;
462 return $json[
'choices'][0][
'message'][
'content'] ??
null;
$propal type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
dolibarr_set_const($db, $name, $value, $type='chaine', $visible=0, $note='', $entity=1)
Insert a parameter (key,value) into database (delete old key then insert it again).
dolibarr_del_const($db, $name, $entity=1)
Delete a constant.
clearModelFailure()
Drop a recorded model failure once that model answers again.
curl(string $url, array $data, array $headers, bool $isClaude=false, bool $isGemini=false)
Execute HTTP Request via cURL.
__construct(string $type, string $key, string $baseUrl, string $model, int $timeout)
Constructor.
generate(string $system, string $userMsg, string $mode='text', array $attachments=array(), array $history=array())
Generate a response using the configured LLM provider.
recordModelFailure(int $httpCode, string $msg)
Record a "model not found / retired" type provider failure into the constant AI_MODEL_RUNTIME_FAILURE...
callGoogle(string $sys, string $msg, string $mode='text', array $attachments=array(), array $history=array())
Call Google Gemini API.
encodeRequestForLog(array $data)
JSON-encode a request for the log with base64 payloads removed, so the 60k truncation in ai_log_reque...
callAnthropic(string $sys, string $msg, string $mode='text', array $attachments=array(), array $history=array())
Call Anthropic API (Claude)
callOpenAI(string $sys, string $msg, string $mode='text', array $attachments=array(), array $history=array())
Call OpenAI-compatible API.
dol_now($mode='gmt')
Return date for now.
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '...' if string larger than length.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).