PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / trunk
AI Engine – The Chatbot, AI Framework & MCP for WordPress vtrunk
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 8 hours ago chatml.php 1 week ago core.php 2 days ago custom.php 1 month ago factory.php 1 week ago google-interactions.php 2 days ago google.php 1 week ago mistral.php 1 week ago open-router.php 3 weeks ago openai.php 3 weeks ago ovh.php 1 week ago perplexity.php 6 months ago replicate.php 5 months ago xai.php 1 month ago
custom.php
216 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 if ( $isEmbedding ) {
148 $modelData['dimensions'] = isset( $remote['dimensions'] ) ? (int) $remote['dimensions'] : 1536;
149 }
150
151 $models[] = $modelData;
152 }
153 return $models;
154 }
155
156 /**
157 * Hits /v1/models directly so the user gets a real success/error response. Works without an
158 * API key, surfaces real HTTP errors when they happen.
159 */
160 public function connection_check() {
161 $base = $this->resolve_endpoint();
162 $url = $base . '/models';
163 $details = [ 'endpoint' => $url ];
164
165 $headers = [ 'User-Agent' => 'AI Engine' ];
166 if ( ! empty( $this->apiKey ) ) {
167 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
168 }
169
170 $response = wp_remote_get( $url, [
171 'headers' => $headers,
172 'timeout' => 10,
173 'sslverify' => MWAI_SSL_VERIFY,
174 ] );
175
176 if ( is_wp_error( $response ) ) {
177 return [
178 'success' => false, 'service' => $this->get_service_name(),
179 'error' => $response->get_error_message(),
180 'details' => $details,
181 ];
182 }
183
184 $code = wp_remote_retrieve_response_code( $response );
185 $body = json_decode( wp_remote_retrieve_body( $response ), true );
186
187 if ( $code >= 400 || ( is_array( $body ) && isset( $body['error'] ) ) ) {
188 $message = is_array( $body ) && isset( $body['error'] )
189 ? ( is_string( $body['error'] ) ? $body['error'] : ( $body['error']['message'] ?? json_encode( $body['error'] ) ) )
190 : "HTTP {$code} from {$url}.";
191 return [
192 'success' => false, 'service' => $this->get_service_name(),
193 'error' => $message,
194 'details' => array_merge( $details, [ 'http_code' => $code ] ),
195 ];
196 }
197
198 $modelIds = [];
199 if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
200 foreach ( array_slice( $body['data'], 0, 5 ) as $m ) {
201 if ( isset( $m['id'] ) ) {
202 $modelIds[] = $m['id'];
203 }
204 }
205 }
206 return [
207 'success' => true, 'service' => $this->get_service_name(),
208 'message' => 'Connection successful. Found ' . count( $body['data'] ?? [] ) . ' models.',
209 'details' => array_merge( $details, [
210 'model_count' => count( $body['data'] ?? [] ),
211 'sample_models' => $modelIds,
212 ] ),
213 ];
214 }
215 }
216