> Raw modelSearch result objects. * @throws \RuntimeException On HTTP error or malformed response for any page. */ public function fetch_all_text_to_image( string $api_key ): array { $all_results = array(); $offset = 0; $pages_fetched = 0; $total = PHP_INT_MAX; // Set after first page. do { $page = $this->fetch_page( $api_key, $offset, self::PAGE_SIZE ); if ( empty( $page['results'] ) ) { break; } $all_results = array_merge( $all_results, $page['results'] ); $total = (int) $page['totalResults']; $offset += self::PAGE_SIZE; ++$pages_fetched; if ( count( $all_results ) >= $total ) { break; } } while ( $pages_fetched < self::PAGE_CAP ); if ( $pages_fetched >= self::PAGE_CAP && count( $all_results ) < $total ) { PRAutoBlogger_Logger::instance()->info( sprintf( 'Runware catalog: reached page cap (%d pages, %d models fetched of %d total). ' . 'Increase PAGE_CAP if curated models are being missed.', self::PAGE_CAP, count( $all_results ), $total ), 'runware-catalog' ); } return $all_results; } /** * Normalize raw modelSearch result objects to the Image_Model_Registry shape. * * Filters to models with "io:text-to-image" in capabilities[]. * Uses the `air` field as the model id (e.g. "runware:100@1"). * Uses the `comment` field as the human-readable description. * cost_per_image is always null here — merged by the catalog class. * * @param array> $raw_models Raw modelSearch results. * @return array> Normalized models. */ public function normalize_models( array $raw_models ): array { $normalized = array(); foreach ( $raw_models as $raw ) { if ( ! is_array( $raw ) ) { continue; } $capabilities = is_array( $raw['capabilities'] ?? null ) ? $raw['capabilities'] : array(); if ( ! in_array( 'io:text-to-image', $capabilities, true ) ) { continue; } $air = (string) ( $raw['air'] ?? '' ); if ( '' === $air ) { continue; } $normalized[] = array( 'id' => $air, 'name' => (string) ( $raw['name'] ?? $air ), 'provider' => 'runware', 'cost_per_image' => null, 'capabilities' => array( 'image_generation' ), 'description' => (string) ( $raw['comment'] ?? '' ), ); } return $normalized; } /** * Fetch a single page of modelSearch results. * * Sends [authentication, modelSearch] task pair to /v1. * A fresh auth task is required per request per Runware's WebSocket-based * API contract (each HTTP POST is a stateless task array). * * @param string $api_key Decrypted Runware API key. * @param int $offset Pagination offset (0-based). * @param int $limit Page size. * @return array{results: array>, totalResults: int} * @throws \RuntimeException On HTTP error or malformed response. */ private function fetch_page( string $api_key, int $offset, int $limit ): array { $payload = array( array( 'taskType' => 'authentication', 'apiKey' => $api_key, ), array( 'taskType' => 'modelSearch', 'taskUUID' => wp_generate_uuid4(), 'offset' => $offset, 'limit' => $limit, ), ); $response = wp_remote_post( self::RUNWARE_API_URL, array( 'timeout' => self::HTTP_TIMEOUT_SEC, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), ) ); if ( is_wp_error( $response ) ) { throw new \RuntimeException( sprintf( 'Runware modelSearch unreachable: %s', $response->get_error_message() ) ); } $status = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $status ) { throw new \RuntimeException( sprintf( 'Runware modelSearch returned HTTP %d', $status ) ); } $raw = (string) wp_remote_retrieve_body( $response ); $decoded = json_decode( $raw, true ); if ( ! is_array( $decoded ) ) { throw new \RuntimeException( 'Runware modelSearch response is not valid JSON.' ); } if ( isset( $decoded['errors'] ) && is_array( $decoded['errors'] ) && ! empty( $decoded['errors'] ) ) { $first_msg = ''; if ( ! empty( $decoded['errors'][0]['message'] ) ) { $first_msg = ': ' . (string) $decoded['errors'][0]['message']; } throw new \RuntimeException( 'Runware modelSearch API error' . $first_msg ); } $data = $decoded['data'] ?? array(); if ( ! is_array( $data ) || empty( $data ) ) { return array( 'results' => array(), 'totalResults' => 0, ); } $item = $data[0]; $results = is_array( $item['results'] ?? null ) ? $item['results'] : array(); $total = isset( $item['totalResults'] ) ? (int) $item['totalResults'] : 0; return array( 'results' => $results, 'totalResults' => $total, ); } }