2424import threading
2525import time
2626import uuid
27- from typing import Any , Optional
27+ from typing import TYPE_CHECKING , Any
2828
2929from nemo_rl .models .generation .openai_server_utils import (
3030 replace_prefix_tokens ,
3131)
3232
33+ if TYPE_CHECKING :
34+ from fastapi import FastAPI
35+
3336logger = logging .getLogger (__name__ )
3437
3538_TOOL_BOT = "<tool_call>\n "
36- _TOOL_CALL_RE = re .compile (
37- r"<tool_call>\n(.*?)\n</tool_call>" , re .DOTALL
38- )
39+ _TOOL_CALL_RE = re .compile (r"<tool_call>\n(.*?)\n</tool_call>" , re .DOTALL )
3940
4041
4142def create_app (
@@ -59,6 +60,7 @@ def create_app(
5960 # so model output starts inside the reasoning block without a leading <think> tag.
6061 # parse() is stateless — safe for concurrent requests.
6162 from tensorrt_llm .llmapi .reasoning_parser import DeepSeekR1Parser
63+
6264 _reasoning_parser = DeepSeekR1Parser (reasoning_at_start = True )
6365
6466 # Stop tokens TRT-LLM appends to gen_token_ids but NOT reproduced by apply_chat_template.
@@ -87,7 +89,10 @@ async def chat_completions(request: Request):
8789 logprobs_requested = body .get ("logprobs" , False )
8890
8991 prompt_token_ids = _build_prompt_token_ids (
90- messages , tokenizer , tools = tools , default_template_kwargs = _default_template_kwargs
92+ messages ,
93+ tokenizer ,
94+ tools = tools ,
95+ default_template_kwargs = _default_template_kwargs ,
9196 )
9297
9398 # Prefix splice: empty required_prefix_ids (no prior assistant turn) → returns template unchanged.
@@ -102,14 +107,18 @@ async def chat_completions(request: Request):
102107 template_token_ids = prompt_token_ids ,
103108 )
104109
105- max_tokens_requested = body .get ("max_tokens" ) or body .get ("max_completion_tokens" ) or max_seq_len
110+ max_tokens_requested = (
111+ body .get ("max_tokens" ) or body .get ("max_completion_tokens" ) or max_seq_len
112+ )
106113 remaining_ctx = max (0 , max_seq_len - len (adj_prompt ))
107114
108115 # Return HTTP 400 on context exhaustion.
109116 if remaining_ctx == 0 :
110117 return JSONResponse (
111118 status_code = 400 ,
112- content = {"error" : f"context length exceeded: prompt ({ len (adj_prompt )} tokens) exhausted context window ({ max_seq_len } )" },
119+ content = {
120+ "error" : f"context length exceeded: prompt ({ len (adj_prompt )} tokens) exhausted context window ({ max_seq_len } )"
121+ },
113122 )
114123
115124 max_tokens = min (int (max_tokens_requested ), remaining_ctx )
@@ -131,7 +140,11 @@ async def chat_completions(request: Request):
131140 )
132141 except Exception as e :
133142 err = str (e )
134- if "prompt length" in err or "max_num_tokens" in err or "max_seq_len" in err :
143+ if (
144+ "prompt length" in err
145+ or "max_num_tokens" in err
146+ or "max_seq_len" in err
147+ ):
135148 return JSONResponse (
136149 status_code = 400 ,
137150 content = {"error" : f"context length exceeded: { err } " },
@@ -144,13 +157,14 @@ async def chat_completions(request: Request):
144157
145158 gen_logprobs : list [float ] = []
146159 if gen .logprobs :
147- for lp in gen .logprobs :
160+ # TRT-LLM returns floats in simple format and token-indexed dicts otherwise.
161+ for token_id , lp in zip (gen_token_ids , gen .logprobs , strict = True ):
148162 if isinstance (lp , (int , float )):
149163 gen_logprobs .append (float (lp ))
150164 elif isinstance (lp , dict ):
151- gen_logprobs .append (float (next ( iter ( lp . values ())) .logprob ))
165+ gen_logprobs .append (float (lp [ token_id ] .logprob ))
152166 else :
153- gen_logprobs . append ( float (lp ))
167+ raise TypeError ( f"Unsupported TRT-LLM logprob type: { type (lp )} " )
154168
155169 # Strip trailing stop tokens TRT-LLM appends — apply_chat_template doesn't reproduce
156170 # <|endoftext|>, so they'd break seen_token_ids contiguity. Trim logprobs in lockstep.
@@ -239,6 +253,7 @@ async def chat_completions(request: Request):
239253# Tool-call parsing — mirrors Qwen3ToolParser.detect_and_parse() exactly
240254# ---------------------------------------------------------------------------
241255
256+
242257def _parse_tool_calls (text : str ) -> list [dict [str , Any ]]:
243258 """Extract <tool_call> blocks; arg serialisation is byte-identical to Qwen3ToolParser."""
244259 results : list [dict [str , Any ]] = []
@@ -253,14 +268,16 @@ def _parse_tool_calls(text: str) -> list[dict[str, Any]]:
253268 obj .get ("parameters" ) or obj .get ("arguments" , {}),
254269 ensure_ascii = False ,
255270 )
256- results .append ({
257- "id" : f"call_{ uuid .uuid4 ().hex [:8 ]} " ,
258- "type" : "function" ,
259- "function" : {
260- "name" : func_name ,
261- "arguments" : arguments ,
262- },
263- })
271+ results .append (
272+ {
273+ "id" : f"call_{ uuid .uuid4 ().hex [:8 ]} " ,
274+ "type" : "function" ,
275+ "function" : {
276+ "name" : func_name ,
277+ "arguments" : arguments ,
278+ },
279+ }
280+ )
264281 return results
265282
266283
@@ -276,6 +293,7 @@ def _strip_tool_call_tags(text: str) -> str:
276293# Prompt construction
277294# ---------------------------------------------------------------------------
278295
296+
279297def _to_int_ids (enc : Any ) -> list [int ]:
280298 """Coerce apply_chat_template output to flat list[int] (handles transformers v5 BatchEncoding)."""
281299 if hasattr (enc , "input_ids" ): # v5 BatchEncoding
@@ -329,17 +347,23 @@ def _compute_splice_inputs(
329347 required_prefix_ids : list [int ] = []
330348 for _m in reversed (messages ):
331349 if _m .get ("role" ) == "assistant" and "prompt_token_ids" in _m :
332- required_prefix_ids = (
333- list ( _m [ "prompt_token_ids" ]) + list ( _m .get ("generation_token_ids" ) or [])
350+ required_prefix_ids = list ( _m [ "prompt_token_ids" ]) + list (
351+ _m .get ("generation_token_ids" ) or []
334352 )
335353 break
336354
337355 _clean = [_strip_token_fields (m ) for m in messages ]
338356 _last_asst_idx = next (
339- (i for i in reversed (range (len (_clean ))) if _clean [i ].get ("role" ) == "assistant" ),
357+ (
358+ i
359+ for i in reversed (range (len (_clean )))
360+ if _clean [i ].get ("role" ) == "assistant"
361+ ),
340362 None ,
341363 )
342- _msgs_to_last_asst = _clean [:_last_asst_idx + 1 ] if _last_asst_idx is not None else _clean
364+ _msgs_to_last_asst = (
365+ _clean [: _last_asst_idx + 1 ] if _last_asst_idx is not None else _clean
366+ )
343367 _prefix_tkw : dict [str , Any ] = {
344368 ** default_template_kwargs ,
345369 "tokenize" : True ,
@@ -357,6 +381,7 @@ def _compute_splice_inputs(
357381# Server lifecycle
358382# ---------------------------------------------------------------------------
359383
384+
360385def start_server (
361386 llm : Any ,
362387 tokenizer : Any ,
@@ -368,6 +393,7 @@ def start_server(
368393) -> "tuple[threading.Thread, str, Any]" :
369394 """Start the HTTP server in a daemon thread and return (thread, base_url, server)."""
370395 import uvicorn
396+
371397 from nemo_rl .distributed .virtual_cluster import (
372398 _get_free_port_local ,
373399 _get_node_ip_local ,
0 commit comments