dolibarr 24.0.0-beta
llmadapter.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2026 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2026 Nick Fragoulis
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY, without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 * or see https://www.gnu.org/
18 */
19
27{
29 public $lastRequest = "";
30
32 public $lastResponse = "";
33
35 private $type;
36
38 private $key;
39
41 private $baseUrl;
42
44 private $model;
45
47 private $timeout;
48
58 public function __construct(string $type, string $key, string $baseUrl, string $model, int $timeout)
59 {
60 $this->type = strtolower($type);
61 $this->key = $key;
62 $this->baseUrl = rtrim($baseUrl, '/');
63 $this->model = $model;
64 $this->timeout = $timeout;
65 }
66
75 public function generate(string $system, string $userMsg, string $mode = 'text'): ?string
76 {
77 switch ($this->type) {
78 case 'anthropic':
79 return $this->callAnthropic($system, $userMsg, $mode);
80 case 'google':
81 return $this->callGoogle($system, $userMsg, $mode);
82 default:
83 return $this->callOpenAI($system, $userMsg, $mode);
84 }
85 }
86
95 private function callOpenAI(string $sys, string $msg, string $mode = 'text'): ?string
96 {
97 $url = $this->baseUrl;
98 if (strpos($url, '/chat/completions') === false && strpos($url, '/generate') === false) {
99 $url .= '/chat/completions';
100 }
101
102 $data = array(
103 "model" => $this->model,
104 "messages" => array(
105 array("role" => "system", "content" => $sys),
106 array("role" => "user", "content" => $msg)
107 ),
108 "temperature" => 0.1
109 );
110
111 // Only force JSON mode if explicitly requested
112 // This allows Email/Webpage generation to return raw HTML
113 if ($mode === 'json') {
114 // Apply to specific providers known to support this parameter safely
115 if (strpos($url, 'openai') !== false || strpos($url, 'deepseek') !== false || strpos($url, 'perplexity') !== false || strpos($url, 'mistral') !== false || strpos($url, 'zai') !== false) {
116 $data["response_format"] = array("type" => "json_object");
117 }
118 }
119
120 $this->lastRequest = json_encode($data, JSON_PRETTY_PRINT);
121
122 return $this->curl($url, $data, array("Content-Type: application/json", "Authorization: Bearer " . $this->key));
123 }
124
134 private function callAnthropic(string $sys, string $msg, string $mode = 'text')
135 {
136
137 $url = $this->baseUrl . (strpos($this->baseUrl, '/messages') === false ? '/messages' : '');
138
139 $data = array(
140 "model" => $this->model,
141 "system" => $sys,
142 "messages" => array(array("role" => "user", "content" => $msg)),
143 "max_tokens" => 1024
144 );
145
146 $this->lastRequest = json_encode($data, JSON_PRETTY_PRINT);
147
148 return $this->curl($url, $data, array("content-type: application/json", "x-api-key: " . $this->key, "anthropic-version: 2023-06-01"), true);
149 }
150
160 private function callGoogle(string $sys, string $msg, string $mode = 'text')
161 {
162 $url = $this->baseUrl;
163
164 // Strict type check for string position
165 if (strpos($url, ':generateContent') === false) {
166 if (strpos($url, '/models/') === false) {
167 $url .= "/models/" . $this->model;
168 }
169 $url .= ":generateContent";
170 }
171
172 $url .= "?key=" . $this->key;
173
174 $data = array(
175 "contents" => array(
176 array("parts" => array(array("text" => $sys . "\nUser: " . $msg)))
177 ),
178 "generationConfig" => array("temperature" => 0.1)
179 );
180
181 $this->lastRequest = json_encode($data, JSON_PRETTY_PRINT);
182
183 return $this->curl($url, $data, array("Content-Type: application/json"), false, true);
184 }
185
196 private function curl(string $url, array $data, array $headers, bool $isClaude = false, bool $isGemini = false): ?string
197 {
198 include_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
199
200 // By default, we accept only external endpoints ($dolibarr_ai_allow_local_endpoints is not set).
201 // To allow local endpoints, we must set $dolibarr_ai_allow_local_endpoints to 1 or 2 in conf.php.
202 global $dolibarr_ai_allow_local_endpoints;
203 $localurl = $dolibarr_ai_allow_local_endpoints ?? 0;
204
205 // Pass $this->timeout as the response timeout so the LLM-specific value configured
206 // at construction time is honored (getURLContent's $timeoutresponse is the 10th arg;
207 // preceding args $ssl_verifypeer=-1 and $timeoutconnect=0 keep their defaults).
208 $result = getURLContent($url, 'POST', json_encode($data), 1, $headers, array('http', 'https'), $localurl, -1, 0, $this->timeout);
209
210 $body = (string) ($result['content'] ?? '');
211 $httpCode = (int) ($result['http_code'] ?? 0);
212 $effectiveUrl = (string) ($result['url'] ?? $url);
213 // Store an enriched payload so the admin Log Viewer ("VIEW LOGS" in the AI Server
214 // MCP setup page) shows something actionable when something goes wrong, not just
215 // a bare "Invalid JSON response from API." with an empty body.
216 $this->lastResponse = "HTTP " . $httpCode . " from " . $effectiveUrl . "\n--- body (" . strlen($body) . " bytes) ---\n" . $body;
217
218 if (!empty($result['curl_error_no'])) {
219 return "Error: cURL #" . $result['curl_error_no'] . " " . $result['curl_error_msg'] . " (url=" . $effectiveUrl . ")";
220 }
221
222 $json = json_decode($body, true);
223
224 if ($json === null && json_last_error() !== JSON_ERROR_NONE) {
225 // Common real-world causes: HTTP 4xx/5xx with empty body, HTML error page
226 // from a proxy, gateway timeout, etc. Surface the HTTP code and a short
227 // body snippet so the admin can diagnose without re-running with curl.
228 $snippet = substr($body, 0, 500);
229 return "Error: Invalid JSON response from API (HTTP " . $httpCode . ", " . strlen($body) . " bytes). Body snippet: " . ($snippet !== '' ? $snippet : '<empty>');
230 }
231
232 if (isset($json['error'])) {
233 $msg = $json['error']['message'] ?? json_encode($json['error']);
234 return "Error: API " . $msg;
235 }
236
237 // Extraction Logic
238 if ($isClaude) {
239 return $json['content'][0]['text'] ?? null;
240 }
241 if ($isGemini) {
242 return $json['candidates'][0]['content']['parts'][0]['text'] ?? null;
243 }
244
245 // Default (OpenAI compatible)
246 return $json['choices'][0]['message']['content'] ?? null;
247 }
248}
curl(string $url, array $data, array $headers, bool $isClaude=false, bool $isGemini=false)
Execute HTTP Request via cURL.
callAnthropic(string $sys, string $msg, string $mode='text')
Call Anthropic API (Claude)
callGoogle(string $sys, string $msg, string $mode='text')
Call Google Gemini API.
__construct(string $type, string $key, string $baseUrl, string $model, int $timeout)
Constructor.
generate(string $system, string $userMsg, string $mode='text')
Generate a response using the configured LLM provider.
callOpenAI(string $sys, string $msg, string $mode='text')
Call OpenAI-compatible API.
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).
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
Definition repair.php:130