-
Notifications
You must be signed in to change notification settings - Fork 754
[Feature]Add output fallback support for OpenAI serving #7942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4594e20
475342d
ed019d0
fb81e76
09a9344
70527e5
630f519
7ba1b73
36e8309
8a41fb8
6355b4a
fa02284
a80e7fb
0fb09b3
89ebc4c
7e834e9
3abdc57
1815223
5be809a
75e1625
22fe166
c75c0b5
679dc59
1555560
a000d1a
42ba2cf
ab7e87d
5652943
8a1a406
7dcedd7
a8a72ad
14a9b24
7f91703
03393c8
d6077b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,7 @@ | |
| from fastdeploy import envs | ||
| from fastdeploy.input.utils import process_stop_token_ids | ||
| from fastdeploy.logger.request_logger import RequestLogLevel, log_request | ||
| from fastdeploy.output.fallback.base import OutputFallbackContext | ||
| from fastdeploy.utils import data_processor_logger, make_choice_id, parse_choice_id | ||
|
|
||
| _SAMPLING_EPS = 1e-5 | ||
|
|
@@ -74,6 +75,15 @@ def __init__(self, model_name_or_path, tokenizer_type="auto", reasoning_parser_o | |
| self.decode_status: Dict[str, list] = {} | ||
| self.model_status_dict: Dict[str, dict] = {} | ||
| self.tool_parser_dict: Dict = {} | ||
| # Per-request "fallback-corrected" stream history. Maintained | ||
| # separately from decode_status so that reasoning/tool parsers can | ||
| # be fed a coherent text stream after on_delta rewrites/holds it. | ||
| self.fallback_decode_status: Dict[str, dict] = {} | ||
| # Output fallback manager. Set externally (e.g. by api_server) once | ||
| # the processor instance is wired up. When set, fallback strategies | ||
| # are applied to the raw decoded stream BEFORE reasoning/tool | ||
| # parsing inside ``process_response_dict_*``. ``None`` disables. | ||
| self.output_fallback_manager = None | ||
| # Token-encode cache shared by all subclasses. | ||
| self._tokenize_cache: OrderedDict = OrderedDict() | ||
| self._tokenize_cache_capacity: int = 128 | ||
|
|
@@ -363,6 +373,7 @@ def process_response_dict_normal(self, response_dict, **kwargs): | |
| req_id = response_dict["request_id"] | ||
| request = kwargs.get("request", None) | ||
| direct_decode = kwargs.get("direct_decode", False) | ||
| output_fallback_manager = self.output_fallback_manager if not kwargs.get("disable_output_fallback") else None | ||
|
|
||
| if is_end and len(token_ids) > 0 and not kwargs.get("include_stop_str_in_output"): | ||
| if token_ids[-1] in self.eos_token_ids: | ||
|
|
@@ -376,6 +387,20 @@ def process_response_dict_normal(self, response_dict, **kwargs): | |
|
|
||
| if is_end: | ||
| full_text = previous_texts + delta_text | ||
|
|
||
| # Apply output fallback to the full raw text BEFORE reasoning / | ||
| # tool parsing so all sub-streams (content, reasoning, tools) | ||
| # benefit from the rewrite. | ||
| if output_fallback_manager is not None and full_text: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 建议 当前 guard 会让 non-streaming 空输出绕过 fallback。
建议去掉 |
||
| context = OutputFallbackContext( | ||
| request=request, | ||
| request_id=req_id, | ||
| stream=False, | ||
| output=response_dict["outputs"], | ||
| full_text=full_text, | ||
| ) | ||
| full_text = output_fallback_manager.apply(full_text, context) | ||
|
|
||
| response_dict["outputs"]["completion_tokens"] = full_text | ||
| response_dict["outputs"]["text"] = full_text | ||
|
|
||
|
|
@@ -411,19 +436,67 @@ def process_response_dict_streaming(self, response_dict, **kwargs): | |
| req_id = response_dict["request_id"] | ||
| token_ids = response_dict["outputs"]["token_ids"] | ||
| request = kwargs.get("request", None) | ||
| # Caller may opt out via ``disable_output_fallback`` (e.g. the | ||
| # response-processor's multimodal streaming branch where the raw text | ||
| # is later wrapped into a ``multipart`` payload). | ||
| output_fallback_manager = self.output_fallback_manager if not kwargs.get("disable_output_fallback") else None | ||
|
|
||
| if is_end and len(token_ids) > 0 and not kwargs.get("include_stop_str_in_output"): | ||
| if token_ids[-1] in self.eos_token_ids: | ||
| token_ids = token_ids[:-1] | ||
|
|
||
| delta_text, previous_token_ids, previous_texts = self.ids2tokens(token_ids, req_id) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Output fallback (applied BEFORE reasoning/tool parsing so that the | ||
| # whole raw stream — content + thinking + tool-call text — gets a | ||
| # chance to be repaired/rewritten). | ||
| # ------------------------------------------------------------------ | ||
| fallback_held = False | ||
| if output_fallback_manager is not None: | ||
| state = self.fallback_decode_status.setdefault(req_id, {"previous_corrected": ""}) | ||
| context = OutputFallbackContext( | ||
| request=request, | ||
| request_id=req_id, | ||
| stream=True, | ||
| output=response_dict["outputs"], | ||
| delta_text=delta_text, | ||
| ) | ||
| decision = output_fallback_manager.on_delta(req_id, delta_text, context) | ||
This comment was marked as outdated.
Sorry, something went wrong. |
||
| if decision.action == "truncate": | ||
| delta_text = decision.text | ||
| response_dict["finished"] = True | ||
| response_dict["outputs"]["fallback_truncated"] = True | ||
| is_end = True | ||
| elif decision.action == "hold": | ||
| fallback_held = True | ||
| delta_text = "" | ||
| else: # send | ||
| delta_text = decision.text | ||
|
|
||
| if is_end and decision.action != "truncate": | ||
| finish_decision = output_fallback_manager.on_finish(req_id, context) | ||
| if finish_decision.text: | ||
| delta_text = (delta_text or "") + finish_decision.text | ||
|
|
||
| # Override the parser-visible history with the corrected stream | ||
| # (held deltas contribute nothing; sent/flushed text is appended | ||
| # in arrival order). token_ids remain raw — parsers tolerate the | ||
| # minor text/token drift introduced by text-rewriting strategies. | ||
| corrected_previous = state["previous_corrected"] | ||
| state["previous_corrected"] = corrected_previous + (delta_text or "") | ||
| previous_texts = corrected_previous | ||
|
|
||
| response_dict["outputs"]["text"] = delta_text | ||
| response_dict["outputs"]["completion_tokens"] = delta_text | ||
| response_dict["outputs"]["skipped"] = False | ||
| response_dict["outputs"]["tool_calls"] = None | ||
| response_dict["outputs"]["reasoning_content"] = "" | ||
|
|
||
| if fallback_held and not is_end: | ||
| response_dict["outputs"]["skipped"] = True | ||
| return response_dict | ||
|
|
||
| if self.reasoning_parser: | ||
| reasoning_delta_message = self.reasoning_parser.extract_reasoning_content_streaming( | ||
| previous_texts, | ||
|
|
@@ -497,6 +570,10 @@ def process_response_dict_streaming(self, response_dict, **kwargs): | |
| del self.tool_parser_dict[req_id] | ||
| if req_id in self.model_status_dict: | ||
| del self.model_status_dict[req_id] | ||
| if req_id in self.fallback_decode_status: | ||
| del self.fallback_decode_status[req_id] | ||
| if output_fallback_manager is not None: | ||
| output_fallback_manager.cleanup(req_id) | ||
|
|
||
| return response_dict | ||
|
|
||
|
|
||
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.