mcp-core.php
11 hours ago
mcp-oauth.php
1 month ago
mcp-rest.php
3 weeks ago
mcp.conf
1 year ago
mcp.js
8 months ago
mcp.md
8 months ago
mcp.php
1 week ago
wpai-connectors.php
2 months ago
wpai-gateway-availability.php
2 months ago
wpai-gateway-directory.php
2 months ago
wpai-gateway-image-model.php
2 months ago
wpai-gateway-model.php
2 months ago
wpai-gateway-providers.php
2 months ago
wpai-gateway.php
2 months ago
wpai-gateway.php
178 lines
| 1 | <?php |
| 2 | /** |
| 3 | * WP AI Client Gateway — replaces the native provider classes in WP's |
| 4 | * `AiClient::defaultRegistry()` with AI Engine-backed adapters, so every |
| 5 | * `wp_ai_client_prompt()->using_provider(...)` call dispatches through |
| 6 | * `Meow_MWAI_Core::run_query()` and lands in AI Engine's Insights. |
| 7 | * |
| 8 | * @package MeowApps |
| 9 | */ |
| 10 | |
| 11 | class Meow_MWAI_Labs_WPAI_Gateway { |
| 12 | |
| 13 | /** Shared reference to AI Engine core (read-only for the model classes). */ |
| 14 | public static $core = null; |
| 15 | |
| 16 | public function __construct( $core ) { |
| 17 | self::$core = $core; |
| 18 | |
| 19 | if ( ! class_exists( '\\WordPress\\AiClient\\AiClient' ) ) { |
| 20 | return; |
| 21 | } |
| 22 | |
| 23 | // Register after the native providers (init 20) and after WP's |
| 24 | // `_wp_connectors_pass_default_keys_to_ai_client`, so our replacements |
| 25 | // land last and stick. |
| 26 | add_action( 'init', [ $this, 'register' ], 25 ); |
| 27 | |
| 28 | // Tiny test endpoint for developers to verify end-to-end dispatch without |
| 29 | // wiring up ChatKit. Hit `admin-ajax.php?action=mwai_wpai_gateway_test`. |
| 30 | add_action( 'wp_ajax_mwai_wpai_gateway_test', [ $this, 'ajax_test' ] ); |
| 31 | } |
| 32 | |
| 33 | public function ajax_test(): void { |
| 34 | if ( ! current_user_can( 'manage_options' ) ) { |
| 35 | wp_send_json_error( [ 'message' => 'forbidden' ], 403 ); |
| 36 | } |
| 37 | $message = isset( $_REQUEST['message'] ) ? wp_unslash( $_REQUEST['message'] ) : 'Say hi in one word.'; |
| 38 | $model = isset( $_REQUEST['model'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['model'] ) ) : ''; |
| 39 | try { |
| 40 | $registry = \WordPress\AiClient\AiClient::defaultRegistry(); |
| 41 | $provider_id = isset( $_REQUEST['provider'] ) ? sanitize_key( wp_unslash( $_REQUEST['provider'] ) ) : ''; |
| 42 | if ( ! $provider_id ) { |
| 43 | foreach ( self::ADAPTERS as $id => $_cls ) { |
| 44 | if ( $registry->hasProvider( $id ) && $registry->isProviderConfigured( $id ) ) { |
| 45 | $provider_id = $id; |
| 46 | break; |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | if ( ! $provider_id ) { |
| 51 | wp_send_json_error( [ 'message' => 'no configured AI Engine adapter found; enable the Connectors takeover' ], 500 ); |
| 52 | } |
| 53 | $provider = $registry->getProviderClassName( $provider_id ); |
| 54 | if ( ! $model ) { |
| 55 | $models = $provider::modelMetadataDirectory()->listModelMetadata(); |
| 56 | if ( empty( $models ) ) { |
| 57 | wp_send_json_error( [ 'message' => 'no models exposed by ' . $provider_id ], 500 ); |
| 58 | } |
| 59 | $model = $models[0]->getId(); |
| 60 | } |
| 61 | $modelInstance = $provider::model( $model ); |
| 62 | $user = new \WordPress\AiClient\Messages\DTO\UserMessage( [ |
| 63 | new \WordPress\AiClient\Messages\DTO\MessagePart( $message ), |
| 64 | ] ); |
| 65 | $result = $modelInstance->generateTextResult( [ $user ] ); |
| 66 | $text = ''; |
| 67 | foreach ( $result->getCandidates() as $c ) { |
| 68 | foreach ( $c->getMessage()->getParts() as $p ) { |
| 69 | if ( $p->getType()->isText() ) { |
| 70 | $text .= $p->getText(); |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | wp_send_json_success( [ |
| 75 | 'model' => $model, |
| 76 | 'reply' => $text, |
| 77 | 'tokens' => [ |
| 78 | 'prompt' => $result->getTokenUsage()->getPromptTokens(), |
| 79 | 'completion' => $result->getTokenUsage()->getCompletionTokens(), |
| 80 | 'total' => $result->getTokenUsage()->getTotalTokens(), |
| 81 | ], |
| 82 | ] ); |
| 83 | } |
| 84 | catch ( \Throwable $e ) { |
| 85 | wp_send_json_error( [ |
| 86 | 'message' => $e->getMessage(), |
| 87 | 'file' => $e->getFile() . ':' . $e->getLine(), |
| 88 | ], 500 ); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | /** Mapping from AiClient provider id → AI Engine adapter class. */ |
| 93 | const ADAPTERS = [ |
| 94 | 'anthropic' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Anthropic::class, |
| 95 | 'openai' => Meow_MWAI_Labs_WPAI_Gateway_Provider_OpenAI::class, |
| 96 | 'google' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Google::class, |
| 97 | 'mistral' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Mistral::class, |
| 98 | 'openrouter' => Meow_MWAI_Labs_WPAI_Gateway_Provider_OpenRouter::class, |
| 99 | 'perplexity' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Perplexity::class, |
| 100 | 'replicate' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Replicate::class, |
| 101 | 'ollama' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Ollama::class, |
| 102 | 'azure' => Meow_MWAI_Labs_WPAI_Gateway_Provider_Azure::class, |
| 103 | ]; |
| 104 | |
| 105 | public function register(): void { |
| 106 | try { |
| 107 | require_once __DIR__ . '/wpai-gateway-availability.php'; |
| 108 | require_once __DIR__ . '/wpai-gateway-directory.php'; |
| 109 | require_once __DIR__ . '/wpai-gateway-model.php'; |
| 110 | require_once __DIR__ . '/wpai-gateway-image-model.php'; |
| 111 | require_once __DIR__ . '/wpai-gateway-providers.php'; |
| 112 | |
| 113 | $registry = \WordPress\AiClient\AiClient::defaultRegistry(); |
| 114 | |
| 115 | // Only fully take over when the Connectors takeover is in managed mode. |
| 116 | // Otherwise, native provider plugins keep handling their own calls. |
| 117 | $managed = get_option( Meow_MWAI_Labs_WPAI_Connectors::OPTION_MODE, 'observe' ) === 'managed'; |
| 118 | if ( ! $managed ) { |
| 119 | return; |
| 120 | } |
| 121 | |
| 122 | foreach ( self::ADAPTERS as $id => $adapter_class ) { |
| 123 | $this->force_replace_provider( $registry, $id, $adapter_class ); |
| 124 | } |
| 125 | } |
| 126 | catch ( \Throwable $e ) { |
| 127 | error_log( '[MWAI_WPAI_GATEWAY] registration failed: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine() ); |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Replace a provider registration in AiClient, even if the id is already |
| 133 | * taken by a native plugin. AiClient exposes no unregister method, so we |
| 134 | * reach into the private registry state via reflection. |
| 135 | */ |
| 136 | private function force_replace_provider( $registry, string $id, string $adapter_class ): void { |
| 137 | try { |
| 138 | $ref = new \ReflectionObject( $registry ); |
| 139 | |
| 140 | // Scrub existing registration for this id (if any). |
| 141 | if ( $registry->hasProvider( $id ) ) { |
| 142 | foreach ( [ 'registeredIdsToClassNames', 'registeredClassNamesToIds', 'providerAuthenticationInstances' ] as $prop_name ) { |
| 143 | if ( ! $ref->hasProperty( $prop_name ) ) { |
| 144 | continue; |
| 145 | } |
| 146 | $prop = $ref->getProperty( $prop_name ); |
| 147 | $prop->setAccessible( true ); |
| 148 | $value = $prop->getValue( $registry ); |
| 149 | if ( $prop_name === 'registeredIdsToClassNames' ) { |
| 150 | $old_class = $value[ $id ] ?? null; |
| 151 | unset( $value[ $id ] ); |
| 152 | $prop->setValue( $registry, $value ); |
| 153 | if ( $old_class ) { |
| 154 | // Also drop reverse map entries for the old class. |
| 155 | foreach ( [ 'registeredClassNamesToIds', 'providerAuthenticationInstances' ] as $rev ) { |
| 156 | if ( ! $ref->hasProperty( $rev ) ) { |
| 157 | continue; |
| 158 | } |
| 159 | $rp = $ref->getProperty( $rev ); |
| 160 | $rp->setAccessible( true ); |
| 161 | $rv = $rp->getValue( $registry ); |
| 162 | unset( $rv[ $old_class ] ); |
| 163 | $rp->setValue( $registry, $rv ); |
| 164 | } |
| 165 | } |
| 166 | break; |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | $registry->registerProvider( $adapter_class ); |
| 172 | } |
| 173 | catch ( \Throwable $e ) { |
| 174 | error_log( "[MWAI_WPAI_GATEWAY] replace $id failed: " . $e->getMessage() ); |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 |