PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / ai-copilot / providers-registry.php

providers-registry.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.1, at includes/ai-copilot/providers-registry.php

443 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — AI provider registry.
4 *
5 * Lets plugins register additional AI back-ends (OpenAI, Anthropic, Gemini,
6 * local models) the AI Copilot can dispatch through. Each provider supplies
7 * a small set of callables that fully encapsulate its wire format; the
8 * shell drives the agentic loop and observability without knowing which
9 * vendor is on the other end.
10 *
11 * Registration timing: hook `desktop_mode_ai_register_providers` (fires on
12 * `init` at default priority). Plugins can also call
13 * {@see desktop_mode_register_ai_provider()} at any time before the first
14 * dispatch — the registry is just an in-memory map.
15 *
16 * Provider contract (every key is required unless noted):
17 *
18 * array(
19 * 'label' => __( 'OpenAI', 'my-plugin' ),
20 * 'description' => __( 'OpenAI Responses API.', 'my-plugin' ), // optional
21 * 'api_key_label' => __( 'OpenAI API key', 'my-plugin' ), // optional
22 * 'api_key_link' => 'https://platform.openai.com/api-keys', // optional
23 * 'default_model' => 'gpt-5.4-nano', // optional
24 * 'capabilities' => array( 'tools', 'structured_output' ), // optional, informational
25 *
26 * // Required: opaque "turn input" factory. Two kinds:
27 * // 'user_message' — payload is the user's query (string)
28 * // 'tool_results' — payload is array of [{call_id, output (json string)}, ...]
29 * // Returns whatever the provider wants to receive in agentic_call().
30 * 'make_turn_input' => 'my_provider_make_turn_input',
31 *
32 * // Required: one turn of the agentic loop.
33 * // Returns array{ text: ?string, function_calls: array, next_state: mixed, raw: array }
34 * // or WP_Error. function_calls items: { name, call_id, arguments (json string) }.
35 * 'agentic_call' => 'my_provider_agentic_call',
36 *
37 * // Required: single-shot structured-output request.
38 * // $messages are chat-style: [{role, content}, ...]. Returns parsed
39 * // array matching $schema, or WP_Error.
40 * 'structured_request' => 'my_provider_structured_request',
41 * )
42 *
43 * @package WPDesktopMode
44 * @since 0.18.0
45 */
46
47 defined( 'ABSPATH' ) || exit;
48
49 /** Default provider id when none is selected. */
50 const DESKTOP_MODE_AI_DEFAULT_PROVIDER = 'openai';
51
52 /** Required callback keys every registered provider must supply. */
53 const DESKTOP_MODE_AI_PROVIDER_REQUIRED_CALLBACKS = array(
54 'make_turn_input',
55 'agentic_call',
56 'structured_request',
57 );
58
59 // ---------------------------------------------------------------------------
60 // Registry storage
61 // ---------------------------------------------------------------------------
62
63 /**
64 * Internal registry accessor — process-scoped map of provider definitions.
65 *
66 * Acts as both reader and writer; we avoid a global by keeping the array
67 * as a static inside this function. Pass an array action to mutate
68 * (`set`, `unset`, `clear`); omit to read.
69 *
70 * @since 0.18.0
71 *
72 * @param string|null $action 'set' | 'unset' | 'clear' | null.
73 * @param string $id Provider id.
74 * @param array|null $def Provider definition (for 'set').
75 * @return array Current registry snapshot.
76 */
77 function desktop_mode_ai_providers_storage( $action = null, $id = '', $def = null ) {
78 static $providers = array();
79
80 if ( 'set' === $action ) {
81 $providers[ $id ] = $def;
82 } elseif ( 'unset' === $action ) {
83 unset( $providers[ $id ] );
84 } elseif ( 'clear' === $action ) {
85 $providers = array();
86 }
87
88 return $providers;
89 }
90
91 // ---------------------------------------------------------------------------
92 // Public registration API
93 // ---------------------------------------------------------------------------
94
95 /**
96 * Register an AI provider.
97 *
98 * @since 0.18.0
99 *
100 * @param string $id Lowercase provider slug (e.g. 'openai', 'anthropic').
101 * Validated with `sanitize_key()`.
102 * @param array $args Provider definition. See file header for the contract.
103 * @return true|WP_Error True on success, WP_Error if the definition is invalid.
104 */
105 function desktop_mode_register_ai_provider( $id, array $args ) {
106 $id = sanitize_key( (string) $id );
107 if ( '' === $id ) {
108 return new WP_Error( 'desktop_mode_ai_provider_id', 'Provider id cannot be empty.' );
109 }
110
111 foreach ( DESKTOP_MODE_AI_PROVIDER_REQUIRED_CALLBACKS as $key ) {
112 if ( ! isset( $args[ $key ] ) || ! is_callable( $args[ $key ] ) ) {
113 return new WP_Error(
114 'desktop_mode_ai_provider_callback',
115 sprintf( 'Provider "%s" is missing required callable "%s".', $id, $key )
116 );
117 }
118 }
119
120 $def = array(
121 'id' => $id,
122 'label' => isset( $args['label'] ) ? (string) $args['label'] : ucfirst( $id ),
123 'description' => isset( $args['description'] ) ? (string) $args['description'] : '',
124 'api_key_label' => isset( $args['api_key_label'] ) ? (string) $args['api_key_label'] : 'API key',
125 'api_key_link' => isset( $args['api_key_link'] ) ? esc_url_raw( (string) $args['api_key_link'] ) : '',
126 'default_model' => isset( $args['default_model'] ) ? (string) $args['default_model'] : '',
127 'capabilities' => isset( $args['capabilities'] ) && is_array( $args['capabilities'] )
128 ? array_values( array_filter( array_map( 'strval', $args['capabilities'] ) ) )
129 : array(),
130 'make_turn_input' => $args['make_turn_input'],
131 'agentic_call' => $args['agentic_call'],
132 'structured_request' => $args['structured_request'],
133 );
134
135 desktop_mode_ai_providers_storage( 'set', $id, $def );
136
137 /**
138 * Fires after a provider has been registered. Useful for telemetry.
139 *
140 * @since 0.18.0
141 *
142 * @param string $id Provider id.
143 * @param array $def Stored provider definition.
144 */
145 do_action( 'desktop_mode_ai_provider_registered', $id, $def );
146
147 return true;
148 }
149
150 /**
151 * Unregister a provider.
152 *
153 * @since 0.18.0
154 *
155 * @param string $id Provider id.
156 * @return bool True if a provider was removed.
157 */
158 function desktop_mode_unregister_ai_provider( $id ) {
159 $id = sanitize_key( (string) $id );
160 $before = desktop_mode_ai_providers_storage();
161 if ( ! isset( $before[ $id ] ) ) {
162 return false;
163 }
164 desktop_mode_ai_providers_storage( 'unset', $id );
165 return true;
166 }
167
168 // ---------------------------------------------------------------------------
169 // Lazy-loading hook
170 // ---------------------------------------------------------------------------
171
172 /**
173 * Fires the registration action exactly once per request.
174 *
175 * Plugins should hook `desktop_mode_ai_register_providers` to register their
176 * providers. We fire it lazily on first lookup so registration order
177 * doesn't depend on plugin load order.
178 *
179 * @since 0.18.0
180 */
181 function desktop_mode_ai_ensure_providers_registered() {
182 static $fired = false;
183 if ( $fired ) {
184 return;
185 }
186 $fired = true;
187
188 /**
189 * Provider registration action — register any custom providers here.
190 *
191 * @since 0.18.0
192 */
193 do_action( 'desktop_mode_ai_register_providers' );
194 }
195
196 // ---------------------------------------------------------------------------
197 // Lookup
198 // ---------------------------------------------------------------------------
199
200 /**
201 * Returns the registered providers in a JS-safe shape (no callables).
202 *
203 * Used by `desktop_mode_shell_config` to populate the OS Settings provider
204 * picker so the dropdown reflects whatever any plugin has registered.
205 *
206 * @since 0.18.0
207 *
208 * @return array<int, array{ id:string, label:string, description:string, api_key_label:string, api_key_link:string, capabilities:array }>
209 */
210 function desktop_mode_ai_get_providers_for_config() {
211 $out = array();
212 foreach ( desktop_mode_ai_get_providers() as $id => $def ) {
213 $out[] = array(
214 'id' => (string) $id,
215 'label' => (string) $def['label'],
216 'description' => (string) $def['description'],
217 'api_key_label' => (string) $def['api_key_label'],
218 'api_key_link' => (string) $def['api_key_link'],
219 'capabilities' => (array) $def['capabilities'],
220 );
221 }
222 return $out;
223 }
224
225 /**
226 * Returns all currently-registered providers.
227 *
228 * @since 0.18.0
229 *
230 * @return array<string, array> Map of provider id → definition.
231 */
232 function desktop_mode_ai_get_providers() {
233 desktop_mode_ai_ensure_providers_registered();
234 return desktop_mode_ai_providers_storage();
235 }
236
237 /**
238 * Returns a single provider by id.
239 *
240 * @since 0.18.0
241 *
242 * @param string $id Provider id.
243 * @return array|null Provider definition, or null if unregistered.
244 */
245 function desktop_mode_ai_get_provider( $id ) {
246 $id = sanitize_key( (string) $id );
247 $providers = desktop_mode_ai_get_providers();
248 return $providers[ $id ] ?? null;
249 }
250
251 /**
252 * Returns the active provider id for a given user.
253 *
254 * Resolution order:
255 * 1. Per-user OS Settings 'provider' field (if it points at a registered
256 * provider).
257 * 2. Platform settings 'provider' field (same check).
258 * 3. DESKTOP_MODE_AI_DEFAULT_PROVIDER ('openai').
259 *
260 * The `desktop_mode_ai_active_provider` filter wraps the resolved value so
261 * a plugin can pin a specific provider per-request (e.g., based on
262 * request_id, query content, or admin capability).
263 *
264 * @since 0.18.0
265 *
266 * @param int $user_id User id (0 for anonymous contexts).
267 * @return string Provider id. Always a string; may not point at a
268 * registered provider if every fallback misses.
269 */
270 function desktop_mode_ai_get_active_provider_id( $user_id ) {
271 $user_id = (int) $user_id;
272 $providers = desktop_mode_ai_get_providers();
273
274 $candidate = '';
275 if ( $user_id > 0 && function_exists( 'desktop_mode_ai_get_settings' ) ) {
276 $ai = desktop_mode_ai_get_settings( $user_id );
277 if ( ! empty( $ai['provider'] ) && isset( $providers[ $ai['provider'] ] ) ) {
278 $candidate = (string) $ai['provider'];
279 }
280 }
281
282 if ( '' === $candidate && function_exists( 'desktop_mode_ai_get_platform_settings' ) ) {
283 $platform = desktop_mode_ai_get_platform_settings();
284 if ( ! empty( $platform['provider'] ) && isset( $providers[ $platform['provider'] ] ) ) {
285 $candidate = (string) $platform['provider'];
286 }
287 }
288
289 if ( '' === $candidate ) {
290 $candidate = DESKTOP_MODE_AI_DEFAULT_PROVIDER;
291 }
292
293 /**
294 * Filter the resolved active-provider id.
295 *
296 * @since 0.18.0
297 *
298 * @param string $candidate Resolved provider id.
299 * @param int $user_id User id (0 for anonymous).
300 */
301 return (string) apply_filters( 'desktop_mode_ai_active_provider', $candidate, $user_id );
302 }
303
304 /**
305 * Returns the active provider definition for a given user, or WP_Error.
306 *
307 * @since 0.18.0
308 *
309 * @param int $user_id
310 * @return array|WP_Error
311 */
312 function desktop_mode_ai_get_active_provider( $user_id ) {
313 $id = desktop_mode_ai_get_active_provider_id( $user_id );
314 $def = desktop_mode_ai_get_provider( $id );
315 if ( null === $def ) {
316 return new WP_Error(
317 'desktop_mode_ai_no_provider',
318 sprintf( 'AI provider "%s" is not registered.', $id ),
319 array( 'provider' => $id )
320 );
321 }
322 return $def;
323 }
324
325 // ---------------------------------------------------------------------------
326 // Dispatch helpers — the shell calls these instead of touching providers
327 // directly. Each one resolves the active provider and forwards.
328 // ---------------------------------------------------------------------------
329
330 /**
331 * Build an opaque turn-input object via the active provider.
332 *
333 * @since 0.18.0
334 *
335 * @param int $user_id User id (used to resolve active provider).
336 * @param string $kind 'user_message' | 'tool_results'.
337 * @param mixed $payload Kind-specific payload (see file header).
338 * @return mixed|WP_Error Turn-input opaque value, or WP_Error.
339 */
340 function desktop_mode_ai_provider_make_turn_input( $user_id, $kind, $payload ) {
341 $provider = desktop_mode_ai_get_active_provider( $user_id );
342 if ( is_wp_error( $provider ) ) {
343 return $provider;
344 }
345 return call_user_func( $provider['make_turn_input'], (string) $kind, $payload );
346 }
347
348 /**
349 * Run one turn of the agentic loop via the active provider.
350 *
351 * @since 0.18.0
352 *
353 * @param int $user_id User id.
354 * @param string $api_key Provider API key.
355 * @param mixed $turn_input Opaque value from {@see desktop_mode_ai_provider_make_turn_input}.
356 * @param array $tools Tool definitions (provider format).
357 * @param array|null $text_format Optional structured-output schema (provider format).
358 * @param string $instructions System prompt.
359 * @param mixed $state Provider-specific continuation state, or null on first turn.
360 * @return array|WP_Error Normalized turn result or WP_Error.
361 */
362 function desktop_mode_ai_provider_agentic_call(
363 $user_id,
364 $api_key,
365 $turn_input,
366 array $tools,
367 $text_format,
368 $instructions,
369 $state = null
370 ) {
371 $provider = desktop_mode_ai_get_active_provider( $user_id );
372 if ( is_wp_error( $provider ) ) {
373 return $provider;
374 }
375
376 $result = call_user_func(
377 $provider['agentic_call'],
378 (string) $api_key,
379 $turn_input,
380 $tools,
381 $text_format,
382 (string) $instructions,
383 $state
384 );
385
386 if ( is_wp_error( $result ) ) {
387 return $result;
388 }
389
390 if ( ! is_array( $result ) ) {
391 return new WP_Error(
392 'desktop_mode_ai_provider_shape',
393 sprintf( 'Provider "%s" agentic_call returned a non-array.', $provider['id'] )
394 );
395 }
396
397 return wp_parse_args(
398 $result,
399 array(
400 'text' => null,
401 'function_calls' => array(),
402 'next_state' => null,
403 'raw' => null,
404 )
405 );
406 }
407
408 /**
409 * Run a single-shot structured-output request via the active provider.
410 *
411 * @since 0.18.0
412 *
413 * @param int $user_id User id.
414 * @param string $api_key Provider API key.
415 * @param array $messages Chat-style messages.
416 * @param array $schema JSON schema.
417 * @param string $schema_name Identifier for the schema.
418 * @param string $model Optional model override; '' lets the provider pick its default.
419 * @return array|WP_Error
420 */
421 function desktop_mode_ai_provider_structured_request(
422 $user_id,
423 $api_key,
424 array $messages,
425 array $schema,
426 $schema_name,
427 $model = ''
428 ) {
429 $provider = desktop_mode_ai_get_active_provider( $user_id );
430 if ( is_wp_error( $provider ) ) {
431 return $provider;
432 }
433
434 return call_user_func(
435 $provider['structured_request'],
436 (string) $api_key,
437 $messages,
438 $schema,
439 (string) $schema_name,
440 (string) $model
441 );
442 }
443