PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.3
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.3, at includes/ai-copilot/providers-registry.php

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