PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.6.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.6.4
3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / custom.php
ai-engine / classes / engines Last commit date
anthropic.php 2 days ago chatml.php 2 days ago core.php 1 day ago custom.php 2 days ago factory.php 1 month ago google-interactions.php 2 days ago google.php 2 days ago mistral.php 1 month ago open-router.php 1 month ago openai.php 2 days ago ovh.php 2 days ago perplexity.php 7 months ago replicate.php 6 months ago xai.php 2 months ago
custom.php
222 lines
1 <?php
2
3 /**
4 * Generic OpenAI-compatible engine. Lets users point AI Engine at any server that speaks the
5 * OpenAI Chat Completions API (Ollama, LM Studio, vLLM, llama.cpp, LocalAI, TGI in OAI mode,
6 * smaller hosted providers, etc.). Endpoint is user-configurable; API key is optional since
7 * many local servers run unauthenticated.
8 */
9 class Meow_MWAI_Engines_Custom extends Meow_MWAI_Engines_ChatML {
10 public function __construct( $core, $env ) {
11 parent::__construct( $core, $env );
12 }
13
14 protected function set_environment() {
15 $env = $this->env;
16 $this->apiKey = $env['apikey'] ?? null;
17 }
18
19 protected function get_service_name() {
20 return !empty( $this->env['name'] ) ? $this->env['name'] : 'Custom';
21 }
22
23 public function get_models() {
24 // Prefer dynamically-fetched models, fall back to whatever the user added manually for this
25 // env type via the Custom Models UI. No static list, since the model lineup is whatever the
26 // user's server happens to expose.
27 return $this->core->get_engine_models( 'custom' );
28 }
29
30 public static function get_models_static() {
31 return [];
32 }
33
34 protected function build_url( $query, $endpoint = null ) {
35 $base = $this->resolve_endpoint();
36 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
37 return $base . '/chat/completions';
38 }
39 if ( $query instanceof Meow_MWAI_Query_Embed ) {
40 return $base . '/embeddings';
41 }
42 throw new Exception( 'Unsupported query type for the Custom (OpenAI-Compatible) provider.' );
43 }
44
45 /**
46 * Bearer auth is optional here. Most local servers (Ollama default install, LM Studio,
47 * llama.cpp server) do not require an API key; hosted OpenAI-compatible endpoints typically
48 * do. Only attach the Authorization header when the user has set a key.
49 */
50 protected function build_headers( $query ) {
51 if ( $query->apiKey ) {
52 $this->apiKey = $query->apiKey;
53 }
54 $headers = [
55 'Content-Type' => 'application/json',
56 'User-Agent' => 'AI Engine',
57 ];
58 if ( !empty( $this->apiKey ) ) {
59 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
60 }
61 return $headers;
62 }
63
64 protected function build_body( $query, $streamCallback = null, $extra = null ) {
65 $body = parent::build_body( $query, $streamCallback, $extra );
66
67 // Most OAI-compatible servers expect the older max_tokens field, not max_completion_tokens.
68 if ( isset( $body['max_completion_tokens'] ) ) {
69 $body['max_tokens'] = $body['max_completion_tokens'];
70 unset( $body['max_completion_tokens'] );
71 }
72
73 return $body;
74 }
75
76 /**
77 * Resolve the endpoint URL. Defaults to the Ollama localhost URL since that is the most
78 * common starting point; users override per-env in settings.
79 */
80 private function resolve_endpoint() {
81 $endpoint = !empty( $this->env['endpoint'] ) ? $this->env['endpoint'] : 'http://localhost:11434/v1';
82 $endpoint = apply_filters( 'mwai_custom_endpoint', $endpoint, $this->env );
83 return rtrim( $endpoint, '/' );
84 }
85
86 /**
87 * Try to discover models from /v1/models. Servers that don't implement this endpoint
88 * (some llama.cpp builds, custom proxies) just return an empty list — users add models
89 * manually through AI Engine's existing custom-models UI.
90 */
91 public function retrieve_models() {
92 $base = $this->resolve_endpoint();
93 $url = $base . '/models';
94
95 $headers = [ 'User-Agent' => 'AI Engine' ];
96 if ( !empty( $this->apiKey ) ) {
97 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
98 }
99
100 $response = wp_remote_get( $url, [
101 'headers' => $headers,
102 'timeout' => 10,
103 'sslverify' => MWAI_SSL_VERIFY,
104 ] );
105
106 if ( is_wp_error( $response ) ) {
107 Meow_MWAI_Logging::log( 'Custom (OpenAI-Compatible): /models fetch failed: ' . $response->get_error_message() );
108 return [];
109 }
110
111 $code = wp_remote_retrieve_response_code( $response );
112 if ( $code >= 400 ) {
113 Meow_MWAI_Logging::log( "Custom (OpenAI-Compatible): /models returned HTTP {$code}." );
114 return [];
115 }
116
117 $body = json_decode( wp_remote_retrieve_body( $response ), true );
118 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
119 return [];
120 }
121
122 $models = [];
123 foreach ( $body['data'] as $remote ) {
124 $modelId = $remote['id'] ?? '';
125 if ( empty( $modelId ) ) {
126 continue;
127 }
128 $isEmbedding = strpos( strtolower( $modelId ), 'embed' ) !== false;
129 $features = $isEmbedding ? [ 'embedding' ] : [ 'completion' ];
130 $tags = [ 'core', $isEmbedding ? 'embedding' : 'chat' ];
131
132 $modelData = [
133 'model' => $modelId,
134 'name' => $modelId,
135 'family' => 'custom',
136 'features' => $features,
137 // Pricing is unknown for self-hosted/third-party servers — zero out so usage tracking
138 // doesn't invent costs. Users can add per-model pricing through the Custom Models UI.
139 'price' => [ 'in' => 0, 'out' => 0 ],
140 'type' => 'token',
141 'unit' => 1 / 1000000,
142 'maxCompletionTokens' => isset( $remote['max_output_tokens'] ) ? (int) $remote['max_output_tokens'] : 4096,
143 'maxContextualTokens' => isset( $remote['context_window'] ) ? (int) $remote['context_window'] : 8192,
144 'tags' => $tags,
145 ];
146
147 // Only report dimensions when the server actually tells us. The OpenAI /models
148 // schema has no such field, so defaulting to 1536 made every embedding model look
149 // like a fixed 1536 one: the UI locked the field and we sent dimensions=1536 to
150 // servers whose model has another native size, or no support for the parameter at
151 // all (bad request). Unknown means the user can type the right value, and we send
152 // nothing until they do.
153 if ( $isEmbedding && isset( $remote['dimensions'] ) ) {
154 $modelData['dimensions'] = (int) $remote['dimensions'];
155 }
156
157 $models[] = $modelData;
158 }
159 return $models;
160 }
161
162 /**
163 * Hits /v1/models directly so the user gets a real success/error response. Works without an
164 * API key, surfaces real HTTP errors when they happen.
165 */
166 public function connection_check() {
167 $base = $this->resolve_endpoint();
168 $url = $base . '/models';
169 $details = [ 'endpoint' => $url ];
170
171 $headers = [ 'User-Agent' => 'AI Engine' ];
172 if ( !empty( $this->apiKey ) ) {
173 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
174 }
175
176 $response = wp_remote_get( $url, [
177 'headers' => $headers,
178 'timeout' => 10,
179 'sslverify' => MWAI_SSL_VERIFY,
180 ] );
181
182 if ( is_wp_error( $response ) ) {
183 return [
184 'success' => false, 'service' => $this->get_service_name(),
185 'error' => $response->get_error_message(),
186 'details' => $details,
187 ];
188 }
189
190 $code = wp_remote_retrieve_response_code( $response );
191 $body = json_decode( wp_remote_retrieve_body( $response ), true );
192
193 if ( $code >= 400 || ( is_array( $body ) && isset( $body['error'] ) ) ) {
194 $message = is_array( $body ) && isset( $body['error'] )
195 ? ( is_string( $body['error'] ) ? $body['error'] : ( $body['error']['message'] ?? json_encode( $body['error'] ) ) )
196 : "HTTP {$code} from {$url}.";
197 return [
198 'success' => false, 'service' => $this->get_service_name(),
199 'error' => $message,
200 'details' => array_merge( $details, [ 'http_code' => $code ] ),
201 ];
202 }
203
204 $modelIds = [];
205 if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
206 foreach ( array_slice( $body['data'], 0, 5 ) as $m ) {
207 if ( isset( $m['id'] ) ) {
208 $modelIds[] = $m['id'];
209 }
210 }
211 }
212 return [
213 'success' => true, 'service' => $this->get_service_name(),
214 'message' => 'Connection successful. Found ' . count( $body['data'] ?? [] ) . ' models.',
215 'details' => array_merge( $details, [
216 'model_count' => count( $body['data'] ?? [] ),
217 'sample_models' => $modelIds,
218 ] ),
219 ];
220 }
221 }
222