*/ private const PLAN_MESSAGES = array( 'subscription_sharing_user_not_eligible' => 'This ChatGPT account is not eligible for plan usage.', 'subscription_sharing_usage_unavailable' => 'ChatGPT could not check your usage, try again shortly.', 'subscription_sharing_user_unavailable' => 'ChatGPT could not check your usage, try again shortly.', ); /** * Error codes that mean the route does not accept a request parameter. * * The first is the plan usage code; the others are the standard API ones. * * @var list */ private const PARAM_REJECTION_CODES = array( 'subscription_sharing_unsupported_capability', 'unsupported_parameter', 'unknown_parameter' ); /** * Builds the message for an error response. * * @since 1.0.0 * * @param int $status HTTP status. * @param string $body Response body. * @param array $headers Response headers with lower-case names. * @return string */ public static function message( int $status, string $body, array $headers ): string { $decoded = json_decode( $body, true ); $message = null; if ( is_array( $decoded ) && isset( $decoded['detail'] ) && is_string( $decoded['detail'] ) && '' !== $decoded['detail'] ) { $message = 'ChatGPT: ' . $decoded['detail']; } elseif ( is_array( $decoded ) && isset( $decoded['error'] ) && is_array( $decoded['error'] ) ) { $message = self::from_error( $decoded['error'] ); } if ( null === $message ) { $message = sprintf( 'ChatGPT returned HTTP %d.', $status ); } $request_id = $headers['x-oai-request-id'] ?? ''; if ( '' !== $request_id ) { $message .= ' (request id ' . $request_id . ')'; } return $message; } /** * Returns error.code from a JSON error body. * * @since 1.0.0 * * @param string $body Response body. * @return string|null */ public static function code( string $body ): ?string { $decoded = json_decode( $body, true ); if ( ! is_array( $decoded ) || ! isset( $decoded['error'] ) || ! is_array( $decoded['error'] ) ) { return null; } $code = $decoded['error']['code'] ?? null; return is_string( $code ) && '' !== $code ? $code : null; } /** * Whether an error body says the route does not support the given parameter. * * @since 1.0.0 * * @param string $body Response body. * @param string $param Request parameter name, for example max_output_tokens. * @return bool True for a detail naming $param, or an error object on $param with a rejection code. */ public static function rejects_param( string $body, string $param ): bool { $decoded = json_decode( $body, true ); if ( ! is_array( $decoded ) ) { return false; } if ( isset( $decoded['detail'] ) && is_string( $decoded['detail'] ) ) { return false !== strpos( $decoded['detail'], $param ); } $error = isset( $decoded['error'] ) && is_array( $decoded['error'] ) ? $decoded['error'] : array(); return isset( $error['param'], $error['code'] ) && $error['param'] === $param && in_array( $error['code'], self::PARAM_REJECTION_CODES, true ); } /** * Tells whether a 400 body rejects the reasoning parameter or its effort value. * * @since 1.1.0 * * @param string $body Response body. * @return bool */ public static function rejects_reasoning( string $body ): bool { $decoded = json_decode( $body, true ); if ( ! is_array( $decoded ) ) { return false; } if ( isset( $decoded['detail'] ) && is_string( $decoded['detail'] ) ) { return false !== stripos( $decoded['detail'], 'reasoning' ); } $error = isset( $decoded['error'] ) && is_array( $decoded['error'] ) ? $decoded['error'] : array(); $param = $error['param'] ?? null; $codes = array_merge( self::PARAM_REJECTION_CODES, array( 'invalid_value', 'unsupported_value' ) ); return is_string( $param ) && 0 === strpos( $param, 'reasoning' ) && isset( $error['code'] ) && in_array( $error['code'], $codes, true ); } /** * Flattens response headers to lower-case names and string values. * * @since 1.0.0 * * @param array> $headers Headers as the SDK or WordPress returns them. * @return array */ public static function lower_headers( array $headers ): array { $flat = array(); foreach ( $headers as $name => $value ) { $flat[ strtolower( (string) $name ) ] = is_array( $value ) ? implode( ', ', $value ) : (string) $value; } return $flat; } /** * Builds the message for an error object, from a body or a failed stream event. * * @since 1.0.0 * * @param array $error The error object. * @return string|null Null when the object carries neither code nor message. */ public static function from_error( array $error ): ?string { $code = is_string( $error['code'] ?? null ) ? $error['code'] : ''; $text = is_string( $error['message'] ?? null ) ? $error['message'] : ''; $param = is_string( $error['param'] ?? null ) ? $error['param'] : ''; if ( 'subscription_sharing_usage_limit_exceeded' === $code ) { return 'Your ChatGPT plan usage limit was reached. Review it at ' . USAGE_URL; } if ( isset( self::PLAN_MESSAGES[ $code ] ) ) { return self::PLAN_MESSAGES[ $code ]; } if ( 'subscription_sharing_unsupported_capability' === $code ) { return 'ChatGPT rejected a request feature: ' . ( '' !== $param ? $param : $text ); } if ( '' === $text ) { return '' === $code ? null : 'ChatGPT: ' . $code; } return 'ChatGPT: ' . $text . ( '' !== $code ? ' (' . $code . ')' : '' ); } }