mcp-core.php
14 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-directory.php
167 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Exposes AI Engine's models to the WP AI framework. |
| 4 | * |
| 5 | * The directory can be scoped to a single engine type (e.g. only the |
| 6 | * OpenAI-engine models), or list every model AI Engine knows when no scope |
| 7 | * is given. Source of truth is `options.ai_engines[].models`. |
| 8 | */ |
| 9 | |
| 10 | use WordPress\AiClient\Messages\Enums\ModalityEnum; |
| 11 | use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface; |
| 12 | use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; |
| 13 | use WordPress\AiClient\Providers\Models\DTO\SupportedOption; |
| 14 | use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; |
| 15 | use WordPress\AiClient\Providers\Models\Enums\OptionEnum; |
| 16 | |
| 17 | class Meow_MWAI_Labs_WPAI_Gateway_Directory implements ModelMetadataDirectoryInterface { |
| 18 | |
| 19 | private ?string $engine_type; |
| 20 | |
| 21 | public function __construct( ?string $engine_type = null ) { |
| 22 | $this->engine_type = $engine_type; |
| 23 | } |
| 24 | |
| 25 | public function listModelMetadata(): array { |
| 26 | $models = []; |
| 27 | foreach ( $this->iterate_models() as $id => $model ) { |
| 28 | $models[] = $this->make_metadata( $id, $model ); |
| 29 | } |
| 30 | return $models; |
| 31 | } |
| 32 | |
| 33 | public function hasModelMetadata( string $modelId ): bool { |
| 34 | foreach ( $this->iterate_models() as $id => $_m ) { |
| 35 | if ( $id === $modelId ) { |
| 36 | return true; |
| 37 | } |
| 38 | } |
| 39 | return false; |
| 40 | } |
| 41 | |
| 42 | public function getModelMetadata( string $modelId ): ModelMetadata { |
| 43 | foreach ( $this->iterate_models() as $id => $model ) { |
| 44 | if ( $id === $modelId ) { |
| 45 | return $this->make_metadata( $id, $model ); |
| 46 | } |
| 47 | } |
| 48 | throw new \WordPress\AiClient\Common\Exception\InvalidArgumentException( |
| 49 | sprintf( 'Unknown AI Engine model: %s', $modelId ) |
| 50 | ); |
| 51 | } |
| 52 | |
| 53 | private function iterate_models(): iterable { |
| 54 | $core = Meow_MWAI_Labs_WPAI_Gateway::$core; |
| 55 | if ( ! $core ) { |
| 56 | return; |
| 57 | } |
| 58 | $options = $core->get_all_options(); |
| 59 | $engines = $options['ai_engines'] ?? []; |
| 60 | foreach ( $engines as $engine ) { |
| 61 | $engine_type = $engine['type'] ?? ''; |
| 62 | if ( $this->engine_type !== null && $engine_type !== $this->engine_type ) { |
| 63 | continue; |
| 64 | } |
| 65 | if ( empty( $engine['models'] ) || ! is_array( $engine['models'] ) ) { |
| 66 | continue; |
| 67 | } |
| 68 | foreach ( $engine['models'] as $m ) { |
| 69 | if ( empty( $m['model'] ) ) { |
| 70 | continue; |
| 71 | } |
| 72 | $id = $m['model']; |
| 73 | $m['_engine_type'] = $engine_type; |
| 74 | yield $id => $m; |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | private function make_metadata( string $id, array $model ): ModelMetadata { |
| 80 | // Phase 3 TODO — gate `webSearch` on a future 'web_search' tag. Low |
| 81 | // priority; native plugins do this by model-id regex today. |
| 82 | |
| 83 | $capabilities = []; |
| 84 | $tags = $model['tags'] ?? []; |
| 85 | $is_image = in_array( 'image', $tags, true ) || in_array( 'image-generation', $tags, true ); |
| 86 | $is_chat = in_array( 'chat', $tags, true ) || in_array( 'completion', $tags, true ); |
| 87 | $has_functions = in_array( 'functions', $tags, true ); |
| 88 | $has_json = in_array( 'json', $tags, true ); |
| 89 | $has_vision = in_array( 'vision', $tags, true ); |
| 90 | $has_files = in_array( 'files', $tags, true ); |
| 91 | $has_audio = in_array( 'audio', $tags, true ); |
| 92 | |
| 93 | if ( $is_image ) { |
| 94 | $capabilities[] = CapabilityEnum::imageGeneration(); |
| 95 | } |
| 96 | if ( $is_chat ) { |
| 97 | $capabilities[] = CapabilityEnum::textGeneration(); |
| 98 | $capabilities[] = CapabilityEnum::chatHistory(); |
| 99 | } |
| 100 | if ( empty( $capabilities ) ) { |
| 101 | // Unknown tag shape: assume text generation as the safest default. |
| 102 | $capabilities[] = CapabilityEnum::textGeneration(); |
| 103 | $capabilities[] = CapabilityEnum::chatHistory(); |
| 104 | } |
| 105 | |
| 106 | // Option support. Base options every chat model accepts; plus a tag-gated |
| 107 | // set for features that engines actually enforce at dispatch time |
| 108 | // (classes/engines/*.php). If we declared these without the tag, the |
| 109 | // WP AI Client would think we support them and the call would fail deep |
| 110 | // in the engine with an opaque message. |
| 111 | $option_methods = [ |
| 112 | 'outputModalities', 'outputMimeType', 'outputSchema', |
| 113 | 'systemInstruction', 'maxTokens', 'temperature', 'topP', 'topK', |
| 114 | 'candidateCount', 'stopSequences', 'frequencyPenalty', 'presencePenalty', |
| 115 | 'logprobs', 'topLogprobs', 'webSearch', |
| 116 | 'outputFileType', 'outputMediaAspectRatio', 'outputMediaOrientation', |
| 117 | 'outputSpeechVoice', 'customOptions', |
| 118 | ]; |
| 119 | // AI Engine's 'json' tag is narrow (OpenAI only today), but every chat |
| 120 | // engine handles structured output — via native JSON mode, tool use, or |
| 121 | // "reply in JSON" prompting. Declaring outputSchema permissively matches |
| 122 | // reality; gating by tag would falsely advertise Claude/Google as |
| 123 | // incapable of JSON responses. |
| 124 | // |
| 125 | // functionDeclarations IS tag-gated because dispatch-time checks in |
| 126 | // classes/engines/*.php actively skip tool payloads for non-`functions` |
| 127 | // models; declaring here would claim support the engine refuses. |
| 128 | if ( $has_functions ) { |
| 129 | $option_methods[] = 'functionDeclarations'; |
| 130 | } |
| 131 | unset( $has_json ); // reserved for future finer-grained JSON-mode gating |
| 132 | $supportedOptions = []; |
| 133 | foreach ( $option_methods as $m ) { |
| 134 | // OptionEnum uses __callStatic, so method_exists is blind. Try/catch |
| 135 | // lets options not in this WP build degrade quietly. |
| 136 | try { |
| 137 | $supportedOptions[] = new SupportedOption( OptionEnum::$m() ); |
| 138 | } |
| 139 | catch ( \Throwable $e ) { |
| 140 | // Skip options not present in this WP build. |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | // inputModalities is declared with the exact combinations we'll accept. |
| 145 | // Anthropic's reference adapter does the same — see |
| 146 | // AnthropicModelMetadataDirectory.php:95-104. Declaring `null` (any) |
| 147 | // would silently let image-only prompts reach a text-only model. |
| 148 | $input_combos = [ [ ModalityEnum::text() ] ]; |
| 149 | if ( $has_vision ) { |
| 150 | $input_combos[] = [ ModalityEnum::text(), ModalityEnum::image() ]; |
| 151 | } |
| 152 | if ( $has_files ) { |
| 153 | $input_combos[] = [ ModalityEnum::text(), ModalityEnum::document() ]; |
| 154 | if ( $has_vision ) { |
| 155 | $input_combos[] = [ ModalityEnum::text(), ModalityEnum::image(), ModalityEnum::document() ]; |
| 156 | } |
| 157 | } |
| 158 | if ( $has_audio ) { |
| 159 | $input_combos[] = [ ModalityEnum::text(), ModalityEnum::audio() ]; |
| 160 | } |
| 161 | $supportedOptions[] = new SupportedOption( OptionEnum::inputModalities(), $input_combos ); |
| 162 | |
| 163 | $name = $model['rawName'] ?? $model['name'] ?? $id; |
| 164 | return new ModelMetadata( $id, is_string( $name ) ? $name : $id, array_values( array_unique( $capabilities, SORT_REGULAR ) ), $supportedOptions ); |
| 165 | } |
| 166 | } |
| 167 |