$message['role'], 'content' => (string) $message['content'], ); } return $input_items; } /** * Preserve empty JSON schema objects when tool definitions are re-encoded in PHP. * * WordPress request parsing converts empty objects to empty arrays. OpenAI tool schemas * require certain fields like parameters/properties to remain objects, not []. * * @param mixed $tools Raw tool definitions from the request. * @return mixed */ public static function normalize_tool_definitions( $tools ) { if ( ! is_array( $tools ) ) { return $tools; } $normalized_tools = array(); foreach ( $tools as $tool ) { if ( ! is_array( $tool ) ) { $normalized_tools[] = $tool; continue; } if ( isset( $tool['parameters'] ) ) { $tool['parameters'] = self::normalize_tool_schema_node( $tool['parameters'], true ); } $normalized_tools[] = $tool; } return $normalized_tools; } /** * Normalize a schema node so object-typed nodes stay objects after JSON encoding. * * @param mixed $node Raw schema node. * @param bool $force_object Whether this node must encode as an object. * @return mixed */ private static function normalize_tool_schema_node( $node, $force_object = false ) { if ( ! is_array( $node ) ) { return $force_object ? new stdClass() : $node; } if ( empty( $node ) ) { return $force_object ? new stdClass() : array(); } $is_assoc = array_keys( $node ) !== range( 0, count( $node ) - 1 ); $normalized = array(); foreach ( $node as $key => $value ) { $child_force_object = false; if ( $is_assoc && in_array( $key, array( 'parameters', 'properties', 'items' ), true ) ) { $child_force_object = 'items' !== $key; } $normalized[ $key ] = self::normalize_tool_schema_node( $value, $child_force_object ); } if ( $force_object || $is_assoc ) { return (object) $normalized; } return $normalized; } /** * Build a stable prompt cache key for repeated AI Editor requests. * * @param string $openai_model OpenAI API model name. * @param string $request_mode Current request mode. * @param string $system_message Final system message. * @return string */ public static function build_prompt_cache_key( $openai_model, $request_mode, $system_message = '' ) { return sprintf( 'aie:model:%1$s:mode:%2$s:prompt:%3$s', sanitize_key( (string) $openai_model ), sanitize_key( (string) $request_mode ), substr( md5( (string) $system_message ), 0, 12 ) ); } /** * Get the prompt cache retention policy for a model. * * @param string $openai_model OpenAI API model name. * @return string */ public static function get_prompt_cache_retention( $openai_model ) { if ( 'gpt-6-astra' === $openai_model || 0 === strpos( (string) $openai_model, 'gpt-5.6' ) ) { return ''; } $extended_cache_models = array( 'gpt-5.5', 'gpt-5.4', ); return in_array( $openai_model, $extended_cache_models, true ) ? '24h' : 'in_memory'; } /** * Extract a small usage summary from an OpenAI Responses API payload. * * @param array $response_data OpenAI response payload. * @return array */ public static function extract_openai_usage_summary( $response_data ) { if ( ! is_array( $response_data ) || empty( $response_data['usage'] ) || ! is_array( $response_data['usage'] ) ) { return array(); } $usage = $response_data['usage']; $input_tokens = isset( $usage['input_tokens'] ) ? (int) $usage['input_tokens'] : 0; $output_tokens = isset( $usage['output_tokens'] ) ? (int) $usage['output_tokens'] : 0; $total_tokens = isset( $usage['total_tokens'] ) ? (int) $usage['total_tokens'] : ( $input_tokens + $output_tokens ); $summary = array( 'input_tokens' => $input_tokens, 'output_tokens' => $output_tokens, 'total_tokens' => $total_tokens, ); if ( ! empty( $usage['input_tokens_details'] ) && is_array( $usage['input_tokens_details'] ) ) { if ( isset( $usage['input_tokens_details']['cached_tokens'] ) ) { $summary['cached_input_tokens'] = (int) $usage['input_tokens_details']['cached_tokens']; } } if ( ! empty( $usage['output_tokens_details'] ) && is_array( $usage['output_tokens_details'] ) ) { if ( isset( $usage['output_tokens_details']['reasoning_tokens'] ) ) { $summary['reasoning_output_tokens'] = (int) $usage['output_tokens_details']['reasoning_tokens']; } } return $summary; } }