*/ public static function get_catalog() { static $catalog = null; if ( null !== $catalog ) { return $catalog; } $path = WTEAM_EXECUTOR_DIR . self::FILE_RELATIVE; $xml = is_readable( $path ) ? file_get_contents( $path ) : false; if ( ! is_string( $xml ) || '' === trim( $xml ) ) { $catalog = self::fallback_catalog(); return $catalog; } $parsed = self::parse_with_simplexml( $xml ); if ( empty( $parsed['models'] ) ) { $parsed = self::parse_controlled_xml_fallback( $xml ); } $catalog = ! empty( $parsed['models'] ) ? $parsed : self::fallback_catalog(); return $catalog; } /** @return string */ public static function get_default_model() { $catalog = self::get_catalog(); $models = isset( $catalog['models'] ) && is_array( $catalog['models'] ) ? $catalog['models'] : []; $default = isset( $catalog['default'] ) ? self::clean_id( (string) $catalog['default'] ) : ''; if ( '' !== $default && isset( $models[ $default ] ) ) { return $default; } foreach ( $models as $id => $model ) { if ( ! empty( $model['recommended'] ) ) { return (string) $id; } } if ( ! empty( $models ) ) { return (string) key( $models ); } return self::FALLBACK_DEFAULT_MODEL; } /** @return array> */ public static function get_models() { $catalog = self::get_catalog(); return isset( $catalog['models'] ) && is_array( $catalog['models'] ) ? $catalog['models'] : []; } /** * @param string $id * @return array|null */ public static function get_model( $id ) { $models = self::get_models(); return isset( $models[ $id ] ) ? $models[ $id ] : null; } /** * @param string $xml * @return array */ private static function parse_with_simplexml( $xml ) { if ( ! function_exists( 'simplexml_load_string' ) ) { return []; } $previous = libxml_use_internal_errors( true ); $root = simplexml_load_string( $xml, 'SimpleXMLElement', LIBXML_NONET | LIBXML_NOCDATA ); libxml_clear_errors(); libxml_use_internal_errors( $previous ); if ( false === $root ) { return []; } $catalog = [ 'currency' => self::clean_text( (string) $root['currency'] ), 'unit' => self::translate_catalog_text( self::clean_text( (string) $root['unit'] ) ), 'updated' => self::clean_text( (string) $root['updated'] ), 'source' => esc_url_raw( (string) $root['source'] ), 'default' => self::clean_id( (string) $root['default'] ), 'models' => [], ]; foreach ( $root->model as $node ) { $model = self::model_from_simplexml( $node, $catalog ); if ( ! empty( $model['id'] ) ) { $catalog['models'][ $model['id'] ] = $model; } } return $catalog; } /** * @param SimpleXMLElement $node * @param array $catalog * @return array */ private static function model_from_simplexml( $node, array $catalog ) { $notes = []; if ( isset( $node->notes->note ) ) { foreach ( $node->notes->note as $note ) { $value = self::clean_text( (string) $note ); if ( '' !== $value ) { $notes[] = self::translate_catalog_text( $value ); } } } return [ 'id' => self::clean_id( (string) $node['id'] ), 'label' => self::clean_text( (string) $node['label'] ), 'family' => self::clean_text( (string) $node['family'] ), 'status' => self::clean_text( (string) $node['status'] ), 'recommended' => self::xml_bool( (string) $node['recommended'] ), 'description' => self::translate_catalog_text( self::clean_text( (string) $node->description ) ), 'input' => self::price( isset( $node->pricing['input'] ) ? (string) $node->pricing['input'] : '' ), 'cached_input' => self::price( isset( $node->pricing['cached_input'] ) ? (string) $node->pricing['cached_input'] : '' ), 'output' => self::price( isset( $node->pricing['output'] ) ? (string) $node->pricing['output'] : '' ), 'context_window' => isset( $node->context_window ) ? max( 0, (int) $node->context_window ) : 0, 'max_output_tokens' => isset( $node->max_output_tokens ) ? max( 0, (int) $node->max_output_tokens ) : 0, 'image_input' => isset( $node->image_input ) ? self::xml_bool( (string) $node->image_input ) : false, 'notes' => $notes, 'currency' => isset( $catalog['currency'] ) ? (string) $catalog['currency'] : 'USD', 'unit' => isset( $catalog['unit'] ) ? (string) $catalog['unit'] : __( '1M text tokens', 'wteam-executor' ), 'updated' => isset( $catalog['updated'] ) ? (string) $catalog['updated'] : '', 'source' => isset( $catalog['source'] ) ? (string) $catalog['source'] : '', ]; } /** * @param string $xml * @return array */ private static function parse_controlled_xml_fallback( $xml ) { $catalog = [ 'currency' => self::root_attribute( $xml, 'currency' ), 'unit' => self::translate_catalog_text( self::root_attribute( $xml, 'unit' ) ), 'updated' => self::root_attribute( $xml, 'updated' ), 'source' => esc_url_raw( html_entity_decode( self::root_attribute( $xml, 'source' ), ENT_QUOTES | ENT_XML1, 'UTF-8' ) ), 'default' => self::clean_id( self::root_attribute( $xml, 'default' ) ), 'models' => [], ]; if ( ! preg_match_all( '/]*)>(.*?)<\/model>/si', $xml, $matches, PREG_SET_ORDER ) ) { return $catalog; } foreach ( $matches as $match ) { $attrs = self::parse_attributes( $match[1] ); $body = $match[2]; $id = self::clean_id( isset( $attrs['id'] ) ? $attrs['id'] : '' ); if ( '' === $id ) { continue; } $pricing_attrs = []; if ( preg_match( '/]*)\/?\s*>/si', $body, $pricing_match ) ) { $pricing_attrs = self::parse_attributes( $pricing_match[1] ); } $notes = []; if ( preg_match_all( '/(.*?)<\/note>/si', $body, $note_matches ) ) { foreach ( $note_matches[1] as $note ) { $note = self::decode_xml_text( $note ); if ( '' !== $note ) { $notes[] = self::translate_catalog_text( $note ); } } } $catalog['models'][ $id ] = [ 'id' => $id, 'label' => self::clean_text( isset( $attrs['label'] ) ? html_entity_decode( $attrs['label'], ENT_QUOTES | ENT_XML1, 'UTF-8' ) : $id ), 'family' => self::clean_text( isset( $attrs['family'] ) ? html_entity_decode( $attrs['family'], ENT_QUOTES | ENT_XML1, 'UTF-8' ) : '' ), 'status' => self::clean_text( isset( $attrs['status'] ) ? html_entity_decode( $attrs['status'], ENT_QUOTES | ENT_XML1, 'UTF-8' ) : '' ), 'recommended' => self::xml_bool( isset( $attrs['recommended'] ) ? $attrs['recommended'] : '' ), 'description' => self::translate_catalog_text( self::extract_tag_text( $body, 'description' ) ), 'input' => self::price( isset( $pricing_attrs['input'] ) ? $pricing_attrs['input'] : '' ), 'cached_input' => self::price( isset( $pricing_attrs['cached_input'] ) ? $pricing_attrs['cached_input'] : '' ), 'output' => self::price( isset( $pricing_attrs['output'] ) ? $pricing_attrs['output'] : '' ), 'context_window' => (int) self::extract_tag_text( $body, 'context_window' ), 'max_output_tokens' => (int) self::extract_tag_text( $body, 'max_output_tokens' ), 'image_input' => self::xml_bool( self::extract_tag_text( $body, 'image_input' ) ), 'notes' => $notes, 'currency' => isset( $catalog['currency'] ) && '' !== $catalog['currency'] ? $catalog['currency'] : 'USD', 'unit' => isset( $catalog['unit'] ) && '' !== $catalog['unit'] ? $catalog['unit'] : __( '1M text tokens', 'wteam-executor' ), 'updated' => isset( $catalog['updated'] ) ? $catalog['updated'] : '', 'source' => isset( $catalog['source'] ) ? $catalog['source'] : '', ]; } return $catalog; } /** @return array */ private static function fallback_catalog() { return [ 'currency' => 'USD', 'unit' => __( '1M text tokens', 'wteam-executor' ), 'updated' => '', 'source' => '', 'default' => self::FALLBACK_DEFAULT_MODEL, 'models' => [ self::FALLBACK_DEFAULT_MODEL => [ 'id' => self::FALLBACK_DEFAULT_MODEL, 'label' => 'GPT-5.6 Terra', 'family' => 'GPT-5.6', 'status' => 'current', 'recommended' => true, 'description' => __( 'Balanced intelligence and cost.', 'wteam-executor' ), 'input' => null, 'cached_input' => null, 'output' => null, 'context_window' => 0, 'max_output_tokens' => 0, 'image_input' => true, 'notes' => [ __( 'Pricing catalog could not be loaded.', 'wteam-executor' ) ], 'currency' => 'USD', 'unit' => __( '1M text tokens', 'wteam-executor' ), 'updated' => '', 'source' => '', ], ], ]; } /** @param string $xml @param string $name @return string */ private static function root_attribute( $xml, $name ) { if ( preg_match( '/]*)>/si', $xml, $match ) ) { $attrs = self::parse_attributes( $match[1] ); return isset( $attrs[ $name ] ) ? html_entity_decode( $attrs[ $name ], ENT_QUOTES | ENT_XML1, 'UTF-8' ) : ''; } return ''; } /** @param string $source @return array */ private static function parse_attributes( $source ) { $attrs = []; if ( preg_match_all( '/([A-Za-z_:][A-Za-z0-9_.:-]*)\s*=\s*(["\'])(.*?)\2/s', $source, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $attrs[ $match[1] ] = html_entity_decode( $match[3], ENT_QUOTES | ENT_XML1, 'UTF-8' ); } } return $attrs; } /** @param string $body @param string $tag @return string */ private static function extract_tag_text( $body, $tag ) { if ( preg_match( '/<' . preg_quote( $tag, '/' ) . '>(.*?)<\/' . preg_quote( $tag, '/' ) . '>/si', $body, $match ) ) { return self::decode_xml_text( $match[1] ); } return ''; } /** @param string $value @return string */ private static function decode_xml_text( $value ) { $value = preg_replace( '//s', '$1', $value ); return self::clean_text( html_entity_decode( wp_strip_all_tags( (string) $value ), ENT_QUOTES | ENT_XML1, 'UTF-8' ) ); } /** * Translate human-readable catalog copy from models.xml while keeping * machine identifiers, model names, currency codes and URLs unchanged. * * @param string $value Catalog text. * @return string */ private static function translate_catalog_text( $value ) { $translations = [ '1M text tokens' => __( '1M text tokens', 'wteam-executor' ), 'Frontier model for complex professional work and the hardest coding/reasoning tasks.' => __( 'Frontier model for complex professional work and the hardest coding/reasoning tasks.', 'wteam-executor' ), 'Recommended Executor default: strong coding and agentic capability with a better cost/performance balance than Sol.' => __( 'Recommended Executor default: strong coding and agentic capability with a better cost/performance balance than Sol.', 'wteam-executor' ), 'Cost-sensitive model for high-volume testing, routine tool routing and cheaper Executor runs.' => __( 'Cost-sensitive model for high-volume testing, routine tool routing and cheaper Executor runs.', 'wteam-executor' ), 'Prompts with more than 272K input tokens are priced at 2x input and 1.5x output for the full request.' => __( 'Prompts with more than 272K input tokens are priced at 2x input and 1.5x output for the full request.', 'wteam-executor' ), 'Cache writes are billed at 1.25x the uncached input token rate.' => __( 'Cache writes are billed at 1.25x the uncached input token rate.', 'wteam-executor' ), ]; $value = (string) $value; return isset( $translations[ $value ] ) ? $translations[ $value ] : $value; } /** @param mixed $value @return string */ private static function clean_text( $value ) { return trim( sanitize_text_field( (string) $value ) ); } /** @param mixed $value @return string */ private static function clean_id( $value ) { $value = trim( sanitize_text_field( (string) $value ) ); return preg_match( '/^[A-Za-z0-9._:-]{1,100}$/', $value ) ? $value : ''; } /** @param mixed $value @return float|null */ private static function price( $value ) { return is_numeric( $value ) ? (float) $value : null; } /** @param mixed $value @return bool */ private static function xml_bool( $value ) { return in_array( strtolower( trim( (string) $value ) ), [ '1', 'true', 'yes', 'on' ], true ); } }