# Copyright (c) ModelScope Contributors. All rights reserved.
import asyncio
import inspect
import multiprocessing
import os
import time
import torch
from contextlib import contextmanager, nullcontext
from copy import copy, deepcopy
from packaging import version
from PIL import Image
from tqdm import tqdm
from transformers import AutoConfig, GenerationConfig
from transformers.utils import is_torch_npu_available
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union

from swift.metrics import Metric
from swift.model import get_processor
from swift.template import Template
from swift.utils import (disable_deepspeed_zero3, get_device, get_dist_setting, get_logger, is_dist,
                         safe_snapshot_download)
from .infer_engine import InferEngine
from .patch import patch_auto_tokenizer
from .protocol import (ChatCompletionResponse, ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice,
                       ChatCompletionStreamResponse, ChatMessage, DeltaMessage, EmbeddingResponse,
                       EmbeddingResponseData, InferRequest, RequestConfig, random_uuid)
from .utils import AdapterRequest, InferStreamer, patch_npu_vllm, patch_vllm_memory_leak, patch_vllm_triton_device_guard

logger = get_logger()
try:
    # After setting the environment variables, import vllm. This way of writing allows lint to pass.
    os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
    os.environ['VLLM_ENGINE_ITERATION_TIMEOUT_S'] = '86400'
    import vllm
    from vllm import AsyncEngineArgs, AsyncLLMEngine, EngineArgs, LLMEngine, SamplingParams
    from vllm.pooling_params import PoolingParams
    try:
        # vLLM v0.12+ uses StructuredOutputsParams
        from vllm.sampling_params import StructuredOutputsParams
    except ImportError:
        # Fallback for older vLLM versions
        from vllm.sampling_params import GuidedDecodingParams as StructuredOutputsParams
except Exception:
    raise

try:
    from vllm.reasoning import ReasoningParserManager
except ImportError:
    ReasoningParserManager = None

dtype_mapping = {torch.float16: 'float16', torch.bfloat16: 'bfloat16', torch.float32: 'float32'}


def _patch_vllm_dp_coordinator_timeout():
    # https://github.com/vllm-project/vllm/pull/37452 introduced a 30-second default timeout,
    # which is prone to timing out in spawn scenarios. Patch it to 180 seconds here.
    try:
        from vllm.v1.engine import coordinator as coordinator_module
    except ImportError:
        return

    coordinator_cls = coordinator_module.DPCoordinator
    if not hasattr(coordinator_cls, '_wait_for_zmq_addrs'):
        return

    if getattr(coordinator_cls, '_swift_timeout_patched', False):
        return

    def _wait_for_zmq_addrs(self, zmq_addr_pipe):
        t0 = time.monotonic()
        try:
            ready = multiprocessing.connection.wait([zmq_addr_pipe, self.proc.sentinel], timeout=180)
            elapsed = time.monotonic() - t0
            if not ready:
                raise RuntimeError(f'DP Coordinator process failed to report ZMQ addresses '
                                   f'within 180s (elapsed={elapsed:.1f}s).')
            try:
                return zmq_addr_pipe.recv()
            except EOFError:
                raise RuntimeError('DP Coordinator process failed during startup.') from None
        finally:
            zmq_addr_pipe.close()

    coordinator_cls._wait_for_zmq_addrs = _wait_for_zmq_addrs
    coordinator_cls._swift_timeout_patched = True


_patch_vllm_dp_coordinator_timeout()


@contextmanager
def _patch_rope_validation_ignore_keys():
    """Accept list-style RoPE validation ignore keys from older vLLM configs.

    vLLM 0.18.x Qwen3.5 configs may pass ``ignore_keys_at_rope_validation``
    as a list, while Transformers 5.x treats it as a set and performs a set
    union during RoPE validation. vLLM release tags from 0.19.0 onward changed
    the Qwen3.5 configs to set literals, but 0.18-based vLLM/vLLM-Ascend stacks
    still need this compatibility layer. See vLLM PR:
    https://github.com/vllm-project/vllm/pull/37338
    """
    from transformers import PretrainedConfig

    origin_convert = getattr(PretrainedConfig, 'convert_rope_params_to_dict', None)
    if origin_convert is None:
        yield
        return

    def convert_rope_params_to_dict(self, ignore_keys_at_rope_validation=None, **kwargs):
        if isinstance(ignore_keys_at_rope_validation, list):
            ignore_keys_at_rope_validation = set(ignore_keys_at_rope_validation)
        return origin_convert(self, ignore_keys_at_rope_validation=ignore_keys_at_rope_validation, **kwargs)

    PretrainedConfig.convert_rope_params_to_dict = convert_rope_params_to_dict
    try:
        yield
    finally:
        PretrainedConfig.convert_rope_params_to_dict = origin_convert


class VllmEngine(InferEngine):

    def __init__(
        self,
        model_id_or_path: str,
        *,
        template: Optional[Template] = None,
        torch_dtype: Optional[torch.dtype] = None,
        adapters: Optional[List[str]] = None,
        use_async_engine: bool = False,
        model_type: Optional[str] = None,
        template_type: Optional[str] = None,
        use_hf: Optional[bool] = None,
        hub_token: Optional[str] = None,
        revision: Optional[str] = None,
        # engine_kwargs
        gpu_memory_utilization: float = 0.9,
        tensor_parallel_size: int = 1,
        pipeline_parallel_size: int = 1,
        enable_expert_parallel: bool = False,
        max_model_len: Optional[int] = None,
        max_num_seqs: int = 256,
        disable_custom_all_reduce: bool = True,
        enforce_eager: bool = False,
        limit_mm_per_prompt: Optional[Dict[str, Any]] = None,
        seed: Optional[int] = None,
        task_type: Optional[str] = None,  # embedding
        disable_cascade_attn: bool = False,
        load_format: str = 'auto',
        mm_processor_cache_gb: Optional[float] = None,
        logprobs_mode: Optional[str] = None,
        speculative_config: Optional[Union[str, dict]] = None,
        # lora
        enable_lora: bool = False,
        max_loras: int = 1,
        max_lora_rank: int = 16,
        enable_prefix_caching: Optional[bool] = None,
        enable_sleep_mode: bool = False,
        distributed_executor_backend: Optional[str] = None,
        quantization: Optional[str] = None,
        # reasoning parser
        reasoning_parser: Optional[str] = None,
        engine_kwargs: Optional[Dict[str, Any]] = None,
        num_labels: Optional[int] = None,
        reranker_use_activation: bool = True,
    ) -> None:
        self.model_id_or_path = model_id_or_path
        self.torch_dtype = torch_dtype
        if isinstance(adapters, str):
            adapters = [adapters]
        self.default_adapter_request = None
        if isinstance(adapters, list) and adapters:
            assert len(adapters) == 1, 'Only one adapter is supported for now.'
            enable_lora = True
            self.default_adapter_request = AdapterRequest('default', adapters[0])
        self.adapters = adapters or []
        self.use_async_engine = use_async_engine
        self.model_type = model_type
        self.use_hf = use_hf
        self.hub_token = hub_token
        self.revision = revision

        self.gpu_memory_utilization = gpu_memory_utilization
        self.tensor_parallel_size = tensor_parallel_size
        self.pipeline_parallel_size = pipeline_parallel_size
        self.enable_expert_parallel = enable_expert_parallel
        self.max_num_seqs = max_num_seqs
        self.disable_custom_all_reduce = disable_custom_all_reduce
        self.enforce_eager = enforce_eager
        self.limit_mm_per_prompt = limit_mm_per_prompt
        self.seed = seed
        self.task_type = task_type
        self.disable_cascade_attn = disable_cascade_attn
        self.load_format = load_format
        self.mm_processor_cache_gb = mm_processor_cache_gb
        self.logprobs_mode = logprobs_mode
        self.speculative_config = speculative_config

        self.enable_lora = enable_lora
        self.max_loras = max_loras
        self.max_lora_rank = max_lora_rank
        self.enable_prefix_caching = enable_prefix_caching
        self.enable_sleep_mode = enable_sleep_mode
        self.distributed_executor_backend = distributed_executor_backend
        self.quantization = quantization
        self.num_labels = num_labels
        self.reranker_use_activation = reranker_use_activation
        self._config_cls = None

        patch_vllm_memory_leak()
        patch_vllm_triton_device_guard()
        self._adapters_pool = {}
        if template is None:
            processor = self._get_processor()
            template = self._get_template(processor, template_type=template_type)
        else:
            safe_snapshot_download(
                model_id_or_path,
                revision=revision,
                download_model=True,
                use_hf=use_hf,
                ignore_patterns=getattr(template.model_meta, 'ignore_patterns', None),
                hub_token=hub_token)
        super().__init__(template)
        if max_model_len is not None:
            self.max_model_len = max_model_len
            logger.info(f'Setting max_model_len: {max_model_len}')
        self._prepare_engine_kwargs(max_model_len, engine_kwargs)
        context = nullcontext()
        if is_torch_npu_available() and (tensor_parallel_size == 1 or pipeline_parallel_size == 1):
            colocate = (
                getattr(self, '_swift_vllm_colocate_runtime', False)
                or self.distributed_executor_backend == 'external_launcher')
            context = patch_npu_vllm(get_device(), colocate=colocate)
        with context:
            self._prepare_engine()
        self._load_generation_config()
        self._fix_vllm_bug()
        self.patch_remove_log()
        self._request_count = 0
        self._prepare_reasoning_parser(reasoning_parser)

    def _get_processor(self):
        return get_processor(
            model_id_or_path=self.model_id_or_path,
            torch_dtype=self.torch_dtype,
            download_model=True,
            model_type=self.model_type,
            use_hf=self.use_hf,
            hub_token=self.hub_token,
            revision=self.revision,
            num_labels=self.num_labels,
            task_type=self.task_type)

    def _prepare_engine(self) -> None:
        with patch_auto_tokenizer(self.tokenizer), self._patch_auto_config(), \
                _patch_rope_validation_ignore_keys(), disable_deepspeed_zero3():
            llm_engine_cls = AsyncLLMEngine if self.use_async_engine else LLMEngine
            engine = llm_engine_cls.from_engine_args(self.engine_args)
        self.engine = engine

    @contextmanager
    def _patch_auto_config(self):
        _old_from_pretrained = AutoConfig.from_pretrained

        def _from_pretrained(*args, **kwargs):
            config = deepcopy(self.config)
            if self._version_ge('0.19'):
                if self.model_type == 'deepseek_v4':
                    return _old_from_pretrained(*args, **kwargs)
                if self._config_cls is None:
                    hf_config = _old_from_pretrained(*args, **kwargs)
                    self._config_cls = hf_config.__class__
                if not isinstance(config, self._config_cls):
                    config.__class__ = self._config_cls
            return config

        AutoConfig.from_pretrained = _from_pretrained
        try:
            yield
        finally:
            AutoConfig.from_pretrained = _old_from_pretrained

    def _prepare_engine_kwargs(self, max_model_len, engine_kwargs) -> None:
        if engine_kwargs is None:
            engine_kwargs = {}
        if self.task_type == 'embedding':
            self.task = 'embed'
        elif self.task_type == 'seq_cls':
            self.task = 'classify'
        elif self.task_type in ('reranker', 'generative_reranker'):
            self.task = 'score'
        disable_log_stats = engine_kwargs.pop('disable_log_stats', True)
        if self.use_async_engine:
            engine_cls = AsyncEngineArgs
        else:
            engine_cls = EngineArgs
        parameters = inspect.signature(engine_cls).parameters
        if self.use_async_engine and 'disable_log_requests' in parameters:
            engine_kwargs['disable_log_requests'] = True
        if 'enable_lora' in parameters and self.enable_lora:
            engine_kwargs['enable_lora'] = self.enable_lora
            engine_kwargs['max_loras'] = self.max_loras
            engine_kwargs['max_lora_rank'] = self.max_lora_rank
        else:
            assert not self.enable_lora, (
                'The current version of vLLM does not support `enable_lora`. Please upgrade vLLM.')

        if 'limit_mm_per_prompt' in parameters and self.limit_mm_per_prompt:
            engine_kwargs['limit_mm_per_prompt'] = self.limit_mm_per_prompt
        else:
            assert not self.limit_mm_per_prompt, (
                'The current version of vLLM does not support `limit_mm_per_prompt`. Please upgrade vLLM.')
        for key in [
                'enable_expert_parallel', 'enable_sleep_mode', 'disable_cascade_attn', 'load_format',
                'mm_processor_cache_gb', 'speculative_config', 'logprobs_mode', 'quantization'
        ]:
            if key in parameters:
                value = getattr(self, key, None)
                if value is not None:
                    engine_kwargs[key] = value
            else:
                logger.warning(f'The current version of vLLM does not support `{key}`. Ignored.')
        for key in ['task', 'seed']:
            val = getattr(self, key, None)
            if val is not None:
                engine_kwargs[key] = val

        model_info = self.model_info
        arch_mapping = {'deepseek_vl2': ['DeepseekVLV2ForCausalLM'], 'chatglm4v': ['GLM4VForCausalLM']}
        if self.model_meta.model_type in arch_mapping:
            architectures = arch_mapping[self.model_meta.model_type]
            engine_kwargs['hf_overrides'] = {'architectures': architectures}
        self.template.set_mode('vllm')
        engine_kwargs.update(self.template.prepare_engine_kwargs())
        if self.enable_prefix_caching is not None:
            engine_kwargs['enable_prefix_caching'] = self.enable_prefix_caching
        engine_args = engine_cls(
            model=self.model_dir,
            dtype=dtype_mapping[model_info.torch_dtype],
            gpu_memory_utilization=self.gpu_memory_utilization,
            tensor_parallel_size=self.tensor_parallel_size,
            pipeline_parallel_size=self.pipeline_parallel_size,
            max_model_len=max_model_len,
            max_num_seqs=self.max_num_seqs,
            disable_log_stats=disable_log_stats,
            disable_custom_all_reduce=self.disable_custom_all_reduce,
            enforce_eager=self.enforce_eager,
            trust_remote_code=True,
            distributed_executor_backend=self.distributed_executor_backend,
            **engine_kwargs,
        )
        self.engine_args = engine_args

    def _prepare_reasoning_parser(self, reasoning_parser: Optional[str]) -> None:
        self.reasoning_parser = None
        if not reasoning_parser:
            return

        # Validate reasoning_parser if provided
        if ReasoningParserManager is None:
            raise ImportError('the version of vLLM is too old, please upgrade vLLM')

        valid_reasoning_parsers = list(ReasoningParserManager.reasoning_parsers.keys())
        if reasoning_parser not in valid_reasoning_parsers:
            raise ValueError(f'Invalid reasoning_parser: {reasoning_parser}. '
                             f'Available parsers: {valid_reasoning_parsers}')
        logger.info(f'Using reasoning_parser: {reasoning_parser}')

        reasoning_parser_cls = ReasoningParserManager.get_reasoning_parser(reasoning_parser)
        self.reasoning_parser = reasoning_parser_cls(self.tokenizer)

    def _fix_vllm_bug(self) -> None:
        # fix vllm==0.4 bug (very slow)
        tokenizer = self.tokenizer
        if self._version_ge(
                '0.4') and not self._version_ge('0.6') and not tokenizer.__class__.__name__.startswith('Cached'):
            _tokenizer_len = len(tokenizer)
            __old_len__ = tokenizer.__class__.__len__

            def __len__(self) -> int:
                if self is tokenizer:
                    return _tokenizer_len
                else:
                    return __old_len__(self)

            tokenizer.__class__.__len__ = __len__

    def _load_generation_config(self) -> None:
        generation_config_path = os.path.join(self.model_dir, 'generation_config.json')
        if os.path.isfile(generation_config_path):
            generation_config = GenerationConfig.from_pretrained(self.model_dir)
            kwargs = generation_config.to_dict()
            max_new_tokens = kwargs.get('max_new_tokens')
            if max_new_tokens is not None:
                kwargs['max_tokens'] = max_new_tokens
            top_k = kwargs.get('top_k')
            if top_k == 0:
                kwargs['top_k'] = -1
            parameters = inspect.signature(SamplingParams).parameters
            for k, v in kwargs.copy().items():
                if k not in parameters or v is None:
                    kwargs.pop(k)
            self.generation_config = SamplingParams(**kwargs)
        else:
            self.generation_config = SamplingParams()

    def _add_stop_words(self, generation_config: SamplingParams, request_config: RequestConfig) -> None:
        template_meta = self.template.template_meta
        stop_words = (request_config.stop or []) + (self.generation_config.stop or []) + template_meta.stop_words
        generation_config.stop = self._get_stop_words(stop_words)
        # stop parameter is not effective in v1 engine (test version: vllm 0.8.5.post)
        generation_config.stop_token_ids = self._get_stop_token_ids(stop_words)

    @staticmethod
    def _version_ge(base_version: str):
        vllm_version = vllm.__version__
        if vllm_version is None or 'dev' in vllm_version:
            return True
        return version.parse(vllm_version) >= version.parse(base_version)

    def _add_adapter(self, adapter_request: Optional[AdapterRequest] = None):
        assert self.enable_lora, f'adapter_request: {adapter_request}, self.enable_lora: {self.enable_lora}'
        from vllm.lora.request import LoRARequest
        adapter_name = adapter_request.name
        adapter_path = adapter_request.path
        if adapter_name in self._adapters_pool:
            lora_request = self._adapters_pool[adapter_name]
        else:
            lora_request = LoRARequest(
                lora_name=adapter_name, lora_path=adapter_path, lora_int_id=len(self._adapters_pool) + 1)
            self._adapters_pool[adapter_name] = lora_request
        return lora_request

    def _add_request(self,
                     inputs: Dict[str, Any],
                     generation_config: SamplingParams,
                     request_id: str,
                     adapter_request: Optional[AdapterRequest] = None):
        kwargs = {}
        adapter_request = adapter_request or self.default_adapter_request
        if adapter_request:
            kwargs['lora_request'] = self._add_adapter(adapter_request)

        input_ids = inputs['input_ids']
        if self._version_ge('0.4.3'):
            llm_inputs = {'prompt_token_ids': input_ids}
            mm_data = {}
            for key in ['images', 'audios', 'videos']:
                media_data = inputs.get(key) or []
                if media_data:
                    if self._version_ge('0.6'):

                        mm_data[key.rstrip('s')] = media_data[0] if (
                            len(media_data) == 1 and
                            # compat qwen3_vl
                            not isinstance(media_data[0], tuple)) else media_data
                    else:
                        assert len(media_data) == 1, (
                            f'The current version of vllm only supports single {key}. Please upgrade to vllm >= 0.6.0')
                        mm_data[key.rstrip('s')] = media_data[0]
            if mm_data:
                llm_inputs['multi_modal_data'] = mm_data
            mm_processor_kwargs = inputs.get('mm_processor_kwargs')
            if mm_processor_kwargs:
                llm_inputs['mm_processor_kwargs'] = mm_processor_kwargs

            has_task_arg = 'task' in inspect.signature(PoolingParams).parameters
            has_activation_arg = 'activation' in inspect.signature(PoolingParams).parameters
            task_mapping = {
                'embedding': 'embed',
                'seq_cls': 'classify',
                'reranker': 'score',
                'generative_reranker': 'score',
            }
            if self.task_type in task_mapping:
                pooling_kwargs = {}
                if has_task_arg:
                    pooling_kwargs['task'] = task_mapping[self.task_type]
                if self.task_type in ('reranker', 'generative_reranker') and \
                        has_activation_arg and self.reranker_use_activation:
                    pooling_kwargs['activation'] = True
                pooling_params = PoolingParams(**pooling_kwargs)
                return self.engine.encode(llm_inputs, pooling_params, request_id)
            elif self.use_async_engine:
                return self.engine.generate(llm_inputs, generation_config, request_id, **kwargs)
            else:
                return self.engine.add_request(request_id, llm_inputs, generation_config, **kwargs)
        else:
            if self.use_async_engine:
                return self.engine.generate(None, generation_config, request_id, input_ids, **kwargs)
            else:
                return self.engine.add_request(request_id, None, generation_config, input_ids, **kwargs)

    def _get_logprobs(self,
                      logprobs_list: Optional[List[Dict[int, float]]],
                      token_ids: List[int],
                      top_logprobs: Optional[int] = None) -> Optional[Dict[str, Any]]:
        if logprobs_list is None or len(token_ids) == 0:
            return None
        if len(token_ids) > 0:
            logprobs_list = logprobs_list[-len(token_ids):]
        for logprobs in logprobs_list:
            for token_id, logprob in logprobs.items():
                logprobs[token_id] = logprob.logprob
        return super()._get_logprobs(logprobs_list, token_ids, top_logprobs)

    def _prepare_generation_config(self, request_config: RequestConfig) -> SamplingParams:
        kwargs = {'max_tokens': request_config.max_tokens}
        for key in ['temperature', 'top_k', 'top_p', 'repetition_penalty']:
            new_value = getattr(request_config, key)
            if new_value is None:
                kwargs[key] = getattr(self.generation_config, key)
            else:
                kwargs[key] = new_value

        # Convert Swift's Chat Completions API style (logprobs: bool, top_logprobs: int)
        # to vLLM's SamplingParams style (logprobs: int)
        # vLLM semantics:
        #   - logprobs=None: no logprobs returned
        #   - logprobs=0: only sampled token's logprob
        #   - logprobs=N: top-N tokens + sampled token (up to N+1 total)
        if request_config.logprobs:
            # If logprobs=True, return log probabilities
            if request_config.top_logprobs is not None and request_config.top_logprobs > 0:
                # Return top_logprobs most likely tokens at each position
                # (plus sampled token if not in top-N)
                kwargs['logprobs'] = request_config.top_logprobs
            else:
                # Return only the sampled token's logprob
                kwargs['logprobs'] = 0

        if request_config.prompt_logprobs is not None:
            kwargs['prompt_logprobs'] = request_config.prompt_logprobs

        # TODO: beam search
        for key in ['n', 'best_of', 'frequency_penalty', 'presence_penalty', 'seed']:
            if hasattr(SamplingParams, key):
                kwargs[key] = getattr(request_config, key)

        # Handle structured outputs (guided decoding)
        # vLLM v0.12+ uses 'structured_outputs' parameter, older versions use 'guided_decoding'
        if request_config.structured_outputs_regex:
            structured_outputs_param = StructuredOutputsParams(regex=request_config.structured_outputs_regex)
            if hasattr(SamplingParams, 'structured_outputs'):
                kwargs['structured_outputs'] = structured_outputs_param
            else:
                # Fallback for older vLLM versions
                kwargs['guided_decoding'] = structured_outputs_param

        res = SamplingParams(**kwargs)

        if hasattr(res, 'output_kind') and res.n > 1:
            # fix n > 1 in V1 Engine
            from vllm.sampling_params import RequestOutputKind
            res.output_kind = RequestOutputKind.FINAL_ONLY
        return res

    @property
    def inner_model(self):
        return self.engine.model_executor.driver_worker.worker.model_runner.model

    @property
    def inner_model_executor(self):
        return self.engine.model_executor

    async def _infer_stream_async(
        self,
        inputs: Dict[str, Any],
        generation_config: SamplingParams,
        adapter_request: Optional[AdapterRequest],
        request_config: RequestConfig,
    ) -> AsyncIterator[ChatCompletionStreamResponse]:
        request_id = random_uuid()
        result_generator = self._add_request(inputs, generation_config, request_id, adapter_request=adapter_request)
        infer_streamers = [
            InferStreamer(self.template, template_inputs=inputs['template_inputs']) for _ in range(generation_config.n)
        ]
        token_idxs = [0 for _ in range(generation_config.n)]
        async for result in result_generator:
            res = self._create_chat_completion_stream_response(result, request_config, request_id, infer_streamers,
                                                               token_idxs)
            if res is None:
                continue
            yield res

    def _create_chat_completion_stream_response(self, result, request_config, request_id, infer_streamers,
                                                token_idxs) -> Optional[ChatCompletionStreamResponse]:
        is_diff = False
        is_finished = False
        for output in result.outputs:
            output.token_ids = list(output.token_ids)
            output.delta_text = infer_streamers[output.index].get_printable_text(output.token_ids, output.finished())
            output.is_finished = output.finish_reason is not None
            is_diff |= bool(output.delta_text)
            is_finished |= output.is_finished
        if not is_diff and not is_finished:
            return

        num_generated_tokens = sum(len(output.token_ids) for output in result.outputs)
        usage_info = self._get_usage_info(len(result.prompt_token_ids), num_generated_tokens)
        choices = []
        previous_texts = [''] * len(result.outputs)
        for output in result.outputs:
            i = output.index
            logprobs = self._get_logprobs(output.logprobs, output.token_ids[token_idxs[i]:],
                                          request_config.top_logprobs)

            # Handle reasoning content in streaming
            delta_content = output.delta_text
            delta_reasoning_content = None

            if self.reasoning_parser and output.delta_text:
                try:
                    # Get token IDs for the delta (new tokens in this step)
                    delta_token_ids = output.token_ids[token_idxs[i]:]
                    previous_token_ids = output.token_ids[:token_idxs[i]]

                    # Get current accumulated text for this output
                    previous_text = previous_texts[i]
                    current_text = previous_text + output.delta_text
                    previous_texts[i] = current_text
                    # Extract reasoning content from the delta
                    delta_message = self.reasoning_parser.extract_reasoning_content_streaming(
                        previous_text, current_text, output.delta_text, previous_token_ids, output.token_ids,
                        delta_token_ids)

                    if delta_message:
                        delta_reasoning_content = delta_message.reasoning_content
                        if delta_message.content:
                            delta_content = delta_message.content
                        else:
                            delta_content = None

                except Exception as e:
                    logger.warning(f'Failed to extract reasoning content in streaming: {e}')
                    # Fallback to original delta_text
                    delta_content = output.delta_text
            token_idxs[i] = len(output.token_ids)

            toolcall = None
            if output.is_finished:
                toolcall = self._get_toolcall(
                    self.template.decode_generate_ids(output.token_ids, **infer_streamers[i].decode_kwargs))

            choice = ChatCompletionResponseStreamChoice(
                index=i,
                delta=DeltaMessage(
                    role='assistant',
                    content=delta_content,
                    reasoning_content=delta_reasoning_content,
                    tool_calls=toolcall),
                finish_reason=output.finish_reason,
                logprobs=logprobs)
            choices.append(choice)
        return ChatCompletionStreamResponse(model=self.model_name, choices=choices, usage=usage_info, id=request_id)

    @staticmethod
    def _format_prompt_logprobs(prompt_logprobs):
        if prompt_logprobs is None:
            return None
        result = []
        for pos_lps in prompt_logprobs:
            if pos_lps is None:
                result.append(None)
            else:
                pos_dict = {}
                for token_id, lp_obj in pos_lps.items():
                    pos_dict[str(token_id)] = {
                        'logprob': lp_obj.logprob,
                        'rank': getattr(lp_obj, 'rank', None),
                        'decoded_token': getattr(lp_obj, 'decoded_token', ''),
                    }
                result.append(pos_dict)
        return result

    def _create_embedding_response(self, result, generation_config, request_id) -> EmbeddingResponse:
        assert result is not None
        embedding = result.outputs.data.cpu().numpy().tolist()
        usage_info = self._get_usage_info(len(result.prompt_token_ids), 0)
        return EmbeddingResponse(
            model=self.model_name, data=[EmbeddingResponseData(embedding=embedding)], usage=usage_info, id=request_id)

    def _create_chat_completion_response(
        self,
        result,
        inputs,
        request_config,
        request_id,
    ) -> ChatCompletionResponse:
        assert result is not None
        num_generated_tokens = sum(len(output.token_ids) for output in result.outputs)
        usage_info = self._get_usage_info(len(result.prompt_token_ids), num_generated_tokens)
        choices = []
        for output in result.outputs:
            output.token_ids = list(output.token_ids)
            response = self.template.decode_generate_ids(output.token_ids, template_inputs=inputs['template_inputs'])

            # Extract reasoning content if reasoning_parser is enabled
            reasoning_content = None
            content = response
            if self.reasoning_parser:
                try:
                    reasoning_content, content = self.reasoning_parser.extract_reasoning_content(
                        response,
                        request=None  # We don't have the original request here
                    )
                except Exception as e:
                    logger.warning(f'Failed to extract reasoning content: {e}')
                    # Fallback to original response
                    content = response

            logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs)
            toolcall = self._get_toolcall(content)  # Use content instead of response for tool calls
            token_ids = output.token_ids if request_config.return_details else None
            choice = ChatCompletionResponseChoice(
                index=output.index,
                message=ChatMessage(
                    role='assistant', content=content, reasoning_content=reasoning_content, tool_calls=toolcall),
                finish_reason=output.finish_reason,
                logprobs=logprobs,
                token_ids=token_ids)
            choices.append(choice)
        prompt_token_ids = None
        images_size = None
        if request_config.return_details:
            prompt_token_ids = result.prompt_token_ids
            images = inputs['template_inputs'].images
            if all(isinstance(image, Image.Image) for image in images):
                images_size = [image.size for image in images]
        formatted_prompt_logprobs = None
        if request_config.prompt_logprobs is not None:
            formatted_prompt_logprobs = self._format_prompt_logprobs(result.prompt_logprobs)
        return ChatCompletionResponse(
            model=self.model_name,
            choices=choices,
            usage=usage_info,
            id=request_id,
            prompt_token_ids=prompt_token_ids,
            prompt_logprobs=formatted_prompt_logprobs,
            images_size=images_size)

    def _create_seq_cls_response(
        self,
        result,
        request_config,
        request_id,
    ) -> ChatCompletionResponse:
        assert result is not None
        choices = []
        preds = result.outputs.data
        if preds.dim() == 1:
            preds = preds.unsqueeze(0)
        if self.task_type == 'seq_cls':
            top_logprobs = request_config.top_logprobs or 20
            preds, logprobs = self.template.decode_seq_cls(preds, top_logprobs)
        else:
            logprobs = [None] * len(preds)
        num_prompt_token_ids = 0
        num_generated_tokens = 0
        for i, pred in enumerate(preds):
            num_prompt_token_ids += len(result.prompt_token_ids)
            num_generated_tokens += 1
            if isinstance(pred, torch.Tensor):
                pred = pred.tolist()
            choices.append(
                ChatCompletionResponseChoice(
                    index=0,
                    message=ChatMessage(role='assistant', content=pred, tool_calls=None),
                    finish_reason='stop',
                    logprobs=logprobs[i]))
        usage_info = self._get_usage_info(num_prompt_token_ids, num_generated_tokens)
        return ChatCompletionResponse(
            model=self.model_name,
            choices=choices,
            usage=usage_info,
            id=request_id,
            prompt_token_ids=result.prompt_token_ids)

    async def _infer_full_async(
        self,
        inputs: Dict[str, Any],
        generation_config: SamplingParams,
        adapter_request: Optional[AdapterRequest],
        request_config: RequestConfig,
        request_id: Optional[str] = None,
    ) -> Union[ChatCompletionResponse, EmbeddingResponse]:
        if request_id is None:
            request_id = random_uuid()
        result_generator = self._add_request(inputs, generation_config, request_id, adapter_request=adapter_request)
        result = None
        async for result in result_generator:
            pass
        if self.task_type == 'embedding':
            return self._create_embedding_response(result, generation_config, request_id)
        elif self.task_type in ('seq_cls', 'reranker', 'generative_reranker'):
            return self._create_seq_cls_response(result, request_config, request_id)
        else:
            return self._create_chat_completion_response(result, inputs, request_config, request_id)

    def _batch_infer_stream(self, *args, **kwargs):
        if hasattr(self.engine, 'engine'):
            self.engine.engine.model_executor.parallel_worker_tasks = None
        return super()._batch_infer_stream(*args, **kwargs)

    def infer(
        self,
        infer_requests: List[InferRequest],
        request_config: Optional[RequestConfig] = None,
        metrics: Optional[List[Metric]] = None,
        *,
        use_tqdm: Optional[bool] = None,
        adapter_request: Optional[AdapterRequest] = None,
    ) -> List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
        if self.use_async_engine:
            return super().infer(
                infer_requests,
                request_config,
                metrics,
                use_tqdm=use_tqdm,
                adapter_request=adapter_request,
            )
        else:
            request_config = deepcopy(request_config or RequestConfig())
            if request_config.stream and len(infer_requests) > 1:
                raise ValueError('If you want to use stream batch inference, you need to set use_async_engine to True.')
            if use_tqdm is None:
                use_tqdm = len(infer_requests) > 1
            rank = get_dist_setting()[0]
            if is_dist() and rank % self.engine_args.tensor_parallel_size != 0:
                use_tqdm = False
            self.template.set_mode('vllm')
            batched_inputs, error_list = self._batch_encode(infer_requests, strict=getattr(self, 'strict', True))
            request_id_list = []
            for i, inputs in enumerate(batched_inputs):
                request_id = str(self._request_count)
                request_id_list.append(request_id)
                self._request_count += 1
                _request_config = deepcopy(request_config)
                self.set_default_max_tokens(_request_config, inputs)
                generation_config = self._prepare_generation_config(_request_config)
                if generation_config.seed is not None:
                    generation_config.seed += i
                self._add_stop_words(generation_config, _request_config)
                self._add_request(inputs, generation_config, request_id, adapter_request=adapter_request)
            prog_bar = tqdm(total=len(batched_inputs), dynamic_ncols=True, disable=not use_tqdm)
            outputs = {}
            if request_config.stream:

                def _gen_wrapper():
                    infer_streamers = [
                        InferStreamer(self.template, template_inputs=inputs['template_inputs'])
                        for _ in range(generation_config.n)
                    ]
                    token_idxs = [0 for _ in range(generation_config.n)]
                    while self.engine.has_unfinished_requests():
                        result = self.engine.step()
                        if not result:
                            continue
                        result = result[0]
                        res = self._create_chat_completion_stream_response(result, request_config, request_id,
                                                                           infer_streamers, token_idxs)
                        if res is None:
                            continue
                        yield res
                        if result.finished:
                            break

                    self._update_metrics(res, metrics)

                return [_gen_wrapper()]
            else:
                while self.engine.has_unfinished_requests():
                    step_outputs = self.engine.step()
                    for output in step_outputs:
                        if output.finished:
                            outputs[output.request_id] = output
                            prog_bar.update()
                prog_bar.close()
                outputs = [outputs[request_id] for request_id in request_id_list]
                res = [
                    self._create_chat_completion_response(result, inputs, request_config, request_id)
                    for request_id, inputs, result in zip(request_id_list, batched_inputs, outputs)
                ]
                self._update_metrics(res, metrics)
                return self._add_error_list(res, error_list)

    async def infer_async(
        self,
        infer_request: InferRequest,
        request_config: Optional[RequestConfig] = None,
        *,
        adapter_request: Optional[AdapterRequest] = None,
        pre_infer_hook=None,
    ) -> Union[ChatCompletionResponse, AsyncIterator[ChatCompletionStreamResponse]]:
        if not self.use_async_engine:
            raise ValueError('If you want to use `infer_async`, you need to pass `use_async_engine` as True.')
        request_config = deepcopy(request_config or RequestConfig())
        self.template.set_mode('vllm')
        loop = asyncio.get_running_loop()
        with torch.inference_mode():
            inputs = await loop.run_in_executor(None, self.template.encode, infer_request, True)
        self.set_default_max_tokens(request_config, inputs)
        generation_config = self._prepare_generation_config(request_config)
        self._add_stop_words(generation_config, request_config)
        kwargs = {
            'inputs': inputs,
            'generation_config': generation_config,
            'adapter_request': adapter_request,
            'request_config': request_config,
        }
        if hasattr(infer_request, 'uuid') and infer_request.uuid:
            # RolloutInferRequest
            kwargs.update({'request_id': infer_request.uuid})
        if pre_infer_hook:
            kwargs = pre_infer_hook(kwargs)
        if request_config.stream:
            return self._infer_stream_async(**kwargs)
        else:
            return await self._infer_full_async(**kwargs)

    @staticmethod
    def patch_remove_log():
        from vllm.engine import async_llm_engine
        if not hasattr(async_llm_engine, '_log_task_completion'):
            return

        async_llm_engine._origin_log_task_completion = async_llm_engine._log_task_completion

        def new_log_task_completion(task, error_callback) -> None:
            try:
                return_value = task.result()
                raise AssertionError(f'The engine background task should never finish without an '
                                     f'exception. {return_value}')
            except asyncio.exceptions.CancelledError:
                pass

        async_llm_engine._log_task_completion = new_log_task_completion
