*/ private const CAPABILITY_MAP = array( 'text→text' => array( 'in' => array( 'text' ), 'out' => array( 'text' ), ), 'text+image→text' => array( 'in' => array( 'text', 'image' ), 'out' => array( 'text' ), ), 'text+audio→text' => array( 'in' => array( 'text', 'audio' ), 'out' => array( 'text' ), ), 'text→image' => array( 'in' => array( 'text' ), 'out' => array( 'image' ), ), 'text→audio' => array( 'in' => array( 'text' ), 'out' => array( 'audio' ), ), 'text→video' => array( 'in' => array( 'text' ), 'out' => array( 'video' ), ), 'text→embedding' => array( 'in' => array( 'text' ), 'out' => array( 'embedding' ), ), ); /** * Normalize raw OpenRouter model records into the standardized shape. * * Unknown architectures degrade to 'text→text' capability — never throws. * Pricing is converted from per-token (OpenRouter format) to per-million. * * @param array $raw_models Raw 'data' array from the OpenRouter API. * * @return array Normalized records, sorted by name ascending. */ public function normalize( array $raw_models ): array { $normalized = array(); foreach ( $raw_models as $model ) { if ( ! is_array( $model ) || ! isset( $model['id'] ) ) { continue; } $input_modalities = (array) ( $model['architecture']['input_modalities'] ?? array( 'text' ) ); $output_modalities = (array) ( $model['architecture']['output_modalities'] ?? array( 'text' ) ); $name_parts = explode( '/', (string) $model['id'], 2 ); // OpenRouter prices are per-token; convert to per-million. $prompt_price = (float) ( $model['pricing']['prompt'] ?? 0 ); $completion_price = (float) ( $model['pricing']['completion'] ?? 0 ); $normalized[] = array( 'id' => (string) $model['id'], 'name' => (string) ( $model['name'] ?? $model['id'] ), 'provider' => $name_parts[0] ?? 'unknown', 'context_length' => (int) ( $model['context_length'] ?? 0 ), 'input_price_per_m' => round( $prompt_price * 1_000_000, 4 ), 'output_price_per_m' => round( $completion_price * 1_000_000, 4 ), 'capabilities' => $this->derive_capabilities( $input_modalities, $output_modalities ), 'deprecated' => false, 'updated_at' => (int) ( $model['created'] ?? 0 ), ); } usort( $normalized, static function ( array $a, array $b ): int { return strcasecmp( $a['name'], $b['name'] ); } ); return $normalized; } /** * Derive capability strings from input/output modality arrays. * * @param string[] $input_modalities e.g. ['text', 'image'] * @param string[] $output_modalities e.g. ['text'] * * @return string[] Matched capability labels. Defaults to ['text→text']. */ private function derive_capabilities( array $input_modalities, array $output_modalities ): array { $caps = array(); foreach ( self::CAPABILITY_MAP as $label => $requirements ) { $input_match = empty( array_diff( $requirements['in'], $input_modalities ) ); $output_match = empty( array_diff( $requirements['out'], $output_modalities ) ); if ( $input_match && $output_match ) { $caps[] = $label; } } return empty( $caps ) ? array( 'text→text' ) : $caps; } }