|null */ public static function getSupportedApiParams( string $model_id ): ?array { $model_data = get_transient( self::cacheKey() ); if ( ! is_array( $model_data ) ) { return null; } foreach ( $model_data as $info_node ) { if ( ! is_array( $info_node ) || ( $info_node['model_name'] ?? null ) !== $model_id ) { continue; } $params = $info_node['model_info']['supported_openai_params'] ?? null; return is_array( $params ) && array() !== $params ? array_values( $params ) : null; } return null; } /** * {@inheritDoc} * * @param HttpMethodEnum $method HTTP method. * @param string $path Request path relative to the base URL. * @param array $headers Request headers. * @param mixed $data Request body data. */ protected function createRequest( HttpMethodEnum $method, string $path, array $headers = array(), $data = null ): Request { $headers['X-Amazee-Client'] = AmazeeIoAiProvider::clientHeaderValue(); return new Request( $method, AmazeeIoAiProvider::url( $path ), $headers, $data, AmazeeIoAiProvider::requestOptions() ); } /** * {@inheritDoc} */ public function getRequestAuthentication(): RequestAuthenticationInterface { try { $core_auth = parent::getRequestAuthentication(); } catch ( \Exception $exception ) { $core_auth = null; } return AmazeeIoAiProvider::resolveRequestAuthentication( $core_auth ); } /** * {@inheritDoc} */ protected function sendListModelsRequest(): array { // Resolve authentication before serving the cached catalog: it throws // when no token is configured anywhere, so a stale cache cannot // report the provider as connected after the token was removed. $this->getRequestAuthentication(); $model_data = get_transient( self::cacheKey() ); if ( ! is_array( $model_data ) ) { $model_data = $this->fetchModelData(); set_transient( self::cacheKey(), $model_data, self::CACHE_TTL ); } $metadata_list = array(); foreach ( $model_data as $info_node ) { $metadata = $this->modelMetadataFromInfo( $info_node ); if ( null !== $metadata ) { $metadata_list[ $metadata->getId() ] = $metadata; } } return $metadata_list; } /** * Fetches the raw model information from the LiteLLM `model/info` endpoint. * * @return list Raw model info nodes. */ private function fetchModelData(): array { $transporter = $this->getHttpTransporter(); $api_req = $this->createRequest( HttpMethodEnum::GET(), 'model/info' ); $api_req = $this->getRequestAuthentication()->authenticateRequest( $api_req ); $api_res = $transporter->send( $api_req ); $this->throwIfNotSuccessful( $api_res ); $body = $api_res->getData(); if ( ! isset( $body['data'] ) || ! is_array( $body['data'] ) ) { return array(); } return array_values( $body['data'] ); } /** * Builds model metadata from a single `model/info` node. * * The `mode` the endpoint reports for a model decides which capabilities it * is exposed with, so a region offering image generation models advertises * image generation without any change here. Capabilities and options within * a mode are derived from the flags reported for the model. * * @param mixed $info_node Raw node from the model info response. */ private function modelMetadataFromInfo( $info_node ): ?ModelMetadata { if ( ! is_array( $info_node ) || ! isset( $info_node['model_name'] ) ) { return null; } $id = $info_node['model_name']; $meta = $info_node['model_info'] ?? null; if ( ! is_array( $meta ) ) { return null; } $mode = $meta['mode'] ?? ''; if ( 'image_generation' === $mode ) { return self::imageModelMetadata( $id ); } // Embedding support ships with php-ai-client 1.4, or with the AI // plugin's SDK overlay on cores bundling an older client. if ( 'embedding' === $mode && interface_exists( EmbeddingGenerationModelInterface::class ) ) { return self::embeddingModelMetadata( $id ); } if ( 'audio_speech' === $mode ) { return self::speechModelMetadata( $id ); } // ponytail: audio_transcription and other modes have no AI client // capability yet; add a branch here once one ships. if ( 'chat' !== $mode ) { return null; } $capabilities = array( CapabilityEnum::textGeneration(), CapabilityEnum::chatHistory(), ); // Core attributes supported by all models. $options = array(); $options[] = new SupportedOption( OptionEnum::systemInstruction() ); $options[] = new SupportedOption( OptionEnum::maxTokens() ); $options[] = new SupportedOption( OptionEnum::temperature() ); $options[] = new SupportedOption( OptionEnum::topP() ); if ( ! empty( $meta['supports_function_calling'] ) || ! empty( $meta['supports_tool_choice'] ) ) { $options[] = new SupportedOption( OptionEnum::functionDeclarations() ); } if ( ! empty( $meta['supports_response_schema'] ) ) { $options[] = new SupportedOption( OptionEnum::outputMimeType(), array( 'text/plain', 'application/json' ) ); $options[] = new SupportedOption( OptionEnum::outputSchema() ); } // Text plus every combination of the extra input types the model // reports. Audio parts are sent inline as `input_audio`, which is how // the AI client does speech to text. $combinations = array( array( ModalityEnum::text() ) ); $extras = array(); if ( ! empty( $meta['supports_vision'] ) ) { $extras[] = ModalityEnum::image(); } if ( ! empty( $meta['supports_audio_input'] ) ) { $extras[] = ModalityEnum::audio(); } foreach ( $extras as $extra ) { foreach ( $combinations as $combination ) { $combinations[] = array_merge( $combination, array( $extra ) ); } } $options[] = new SupportedOption( OptionEnum::inputModalities(), $combinations ); $options[] = new SupportedOption( OptionEnum::outputModalities(), array( array( ModalityEnum::text() ) ) ); return new ModelMetadata( $id, $id, $capabilities, $options ); } /** * Builds metadata for a model the endpoint reports as `image_generation`. * * The `supported_openai_params` list these models report describes the chat * API rather than `images/generations`, so it is not used here. Only the * inline file type is advertised, matching the base64 payload the endpoint * returns. * * @param string $id Model identifier. */ private static function imageModelMetadata( string $id ): ModelMetadata { $options = array( new SupportedOption( OptionEnum::inputModalities(), array( array( ModalityEnum::text() ) ) ), new SupportedOption( OptionEnum::outputModalities(), array( array( ModalityEnum::image() ) ) ), new SupportedOption( OptionEnum::outputFileType(), array( FileTypeEnum::inline() ) ), ); return new ModelMetadata( $id, $id, array( CapabilityEnum::imageGeneration() ), $options ); } /** * Builds metadata for a model the endpoint reports as `embedding`. * * @param string $id Model identifier. */ private static function embeddingModelMetadata( string $id ): ModelMetadata { $options = array( new SupportedOption( OptionEnum::inputModalities(), array( array( ModalityEnum::text() ) ) ), new SupportedOption( OptionEnum::dimensions() ), new SupportedOption( OptionEnum::customOptions() ), ); return new ModelMetadata( $id, $id, array( CapabilityEnum::embeddingGeneration() ), $options ); } /** * Builds metadata for a model the endpoint reports as `audio_speech`. * * @param string $id Model identifier. */ private static function speechModelMetadata( string $id ): ModelMetadata { $options = array( new SupportedOption( OptionEnum::inputModalities(), array( array( ModalityEnum::text() ) ) ), new SupportedOption( OptionEnum::outputModalities(), array( array( ModalityEnum::audio() ) ) ), new SupportedOption( OptionEnum::outputMimeType(), array_keys( AmazeeIoSpeechModel::FORMATS ) ), new SupportedOption( OptionEnum::outputFileType(), array( FileTypeEnum::inline() ) ), new SupportedOption( OptionEnum::outputSpeechVoice() ), new SupportedOption( OptionEnum::customOptions() ), ); return new ModelMetadata( $id, $id, array( CapabilityEnum::textToSpeechConversion() ), $options ); } /** * {@inheritDoc} * * @param Response $response HTTP response to parse. */ protected function parseResponseToModelMetadataList( Response $response ): array { return array(); } }