@@ -341,8 +341,10 @@ class GenerationArguments:
341341
342342 max_tokens : int
343343 num_draft_tokens : int
344- logprobs : int
344+ logprobs : bool
345+ top_logprobs : int
345346 seed : Optional [int ]
347+ chat_template_kwargs : Optional [Dict [str , Any ]]
346348
347349
348350@dataclass
@@ -382,7 +384,7 @@ class Response:
382384 token : int
383385 logprob : float
384386 finish_reason : Optional [str ]
385- top_tokens : Optional [ Tuple [int , float ]]
387+ top_tokens : Tuple [Dict [ str , Any ]]
386388
387389
388390class TimeBudget :
@@ -557,6 +559,20 @@ def _make_logits_processors(args):
557559 )
558560
559561
562+ def _format_top_logprobs (logprobs , top_logprobs , tokenizer ) -> Tuple [Dict [str , Any ]]:
563+ """Returns info dicts for the top `top_logprobs` tokens from `logprobs`"""
564+ if top_logprobs <= 0 :
565+ return ()
566+ sorted_indices = mx .argpartition (- logprobs , kth = top_logprobs - 1 )
567+ top_indices = sorted_indices [:top_logprobs ].tolist ()
568+ top_logprobs = logprobs [top_indices ].tolist ()
569+ txts = tokenizer .convert_ids_to_tokens (top_indices )
570+ return tuple (
571+ {"id" : i , "token" : s , "logprob" : g }
572+ for i , s , g in zip (top_indices , txts , top_logprobs )
573+ )
574+
575+
560576class ResponseGenerator :
561577 def __init__ (self , model_provider : ModelProvider , prompt_cache : LRUPromptCache ):
562578 self .model_provider = model_provider
@@ -625,7 +641,7 @@ def _share_request(self, request):
625641 rq = request [0 ] if request is not None else Queue ()
626642 return rq , * shareable
627643
628- def _tokenize (self , tokenizer , request ):
644+ def _tokenize (self , tokenizer , request , args ):
629645 if request .request_type == "chat" :
630646 messages = request .messages
631647 tools = request .tools
@@ -640,12 +656,16 @@ def _tokenize(self, tokenizer, request):
640656 "https://github.com/ml-explore/mlx-lm/issues"
641657 )
642658
659+ chat_template_args = self .model_provider .cli_args .chat_template_args
660+ if args .chat_template_kwargs :
661+ chat_template_args = chat_template_args .copy ()
662+ chat_template_args .update (args .chat_template_kwargs )
643663 return tokenizer .apply_chat_template (
644664 messages ,
645665 tools = tools ,
646666 add_generation_prompt = True ,
647667 tokenize = True ,
648- ** self . model_provider . cli_args . chat_template_args ,
668+ ** chat_template_args ,
649669 )
650670 else :
651671 return tokenizer .encode (convert_chat (messages , role_mapping ))
@@ -708,7 +728,12 @@ def progress_callback(info):
708728 and current_model == args .model
709729 and is_batchable
710730 ):
711- prompt = self ._tokenize (current_tokenizer , request )
731+ try :
732+ prompt = self ._tokenize (current_tokenizer , request , args )
733+ except Exception as e :
734+ rqueue .put (e )
735+ continue
736+
712737 ctx = GenerationContext (
713738 has_tool_calling = tokenizer .has_tool_calling ,
714739 tool_call_start = tokenizer .tool_call_start ,
@@ -810,24 +835,15 @@ def progress_callback(info):
810835 if r .finish_reason != "stop" :
811836 result ["detokenizer" ].add_token (r .token )
812837
813- top_tokens = None
814- if args .logprobs > 0 :
815- sorted_indices = mx .argpartition (
816- - r .logprobs , kth = args .logprobs - 1
817- )
818- top_indices = sorted_indices [: args .logprobs ]
819- top_logprobs = r .logprobs [top_indices ]
820- top_token_info = zip (
821- top_indices .tolist (), top_logprobs .tolist ()
822- )
823- top_tokens = tuple (top_token_info )
824838 result ["rqueue" ].put (
825839 Response (
826840 result ["detokenizer" ].last_segment ,
827841 r .token ,
828842 r .logprobs [r .token ].item (),
829843 r .finish_reason ,
830- top_tokens ,
844+ _format_top_logprobs (
845+ r .logprobs , args .top_logprobs , current_tokenizer
846+ ),
831847 )
832848 )
833849
@@ -871,7 +887,7 @@ def progress(tokens_processed, tokens_total):
871887 draft_model = self .model_provider .draft_model
872888
873889 # Prepare the prompt
874- prompt = self ._tokenize (tokenizer , request )
890+ prompt = self ._tokenize (tokenizer , request , args )
875891
876892 # Start the generation context
877893 ctx = GenerationContext (
@@ -923,23 +939,15 @@ def progress(tokens_processed, tokens_total):
923939 num_draft_tokens = args .num_draft_tokens ,
924940 prompt_progress_callback = progress ,
925941 ):
926- top_tokens = None
927- if args .logprobs > 0 :
928- sorted_indices = mx .argpartition (
929- - gen .logprobs , kth = args .logprobs - 1
930- )
931- top_indices = sorted_indices [: args .logprobs ]
932- top_logprobs = gen .logprobs [top_indices ]
933- top_token_info = zip (top_indices .tolist (), top_logprobs .tolist ())
934- top_tokens = tuple (top_token_info )
935-
936942 rqueue .put (
937943 Response (
938944 gen .text ,
939945 gen .token ,
940946 gen .logprobs [gen .token ].item (),
941947 gen .finish_reason ,
942- top_tokens ,
948+ _format_top_logprobs (
949+ gen .logprobs , args .top_logprobs , tokenizer
950+ ),
943951 )
944952 )
945953 cache_key .append (gen .token )
@@ -1052,6 +1060,7 @@ def do_POST(self):
10521060 except json .JSONDecodeError as e :
10531061 logging .error (f"JSONDecodeError: { e } - Raw body: { raw_body .decode ()} " )
10541062 self ._set_completion_headers (400 )
1063+ self .end_headers ()
10551064 self .wfile .write (
10561065 json .dumps ({"error" : f"Invalid JSON in request body: { e } " }).encode ()
10571066 )
@@ -1088,8 +1097,10 @@ def do_POST(self):
10881097 self .xtc_probability = self .body .get ("xtc_probability" , 0.0 )
10891098 self .xtc_threshold = self .body .get ("xtc_threshold" , 0.0 )
10901099 self .logit_bias = self .body .get ("logit_bias" , None )
1091- self .logprobs = self .body .get ("logprobs" , - 1 )
1100+ self .logprobs = self .body .get ("logprobs" , False )
1101+ self .top_logprobs = self .body .get ("top_logprobs" , - 1 )
10921102 self .seed = self .body .get ("seed" , None )
1103+ self .chat_template_kwargs = self .body .get ("chat_template_kwargs" )
10931104 self .validate_model_parameters ()
10941105
10951106 # Get stop sequences
@@ -1132,9 +1143,12 @@ def validate_model_parameters(self):
11321143 ):
11331144 raise ValueError ("repetition_penalty must be a non-negative float" )
11341145
1135- if self .logprobs != - 1 and not (0 < self .logprobs <= 10 ):
1146+ if not isinstance (self .logprobs , bool ):
1147+ raise ValueError ("logprobs must be a boolean" )
1148+
1149+ if self .top_logprobs != - 1 and not (0 < self .top_logprobs <= 10 ):
11361150 raise ValueError (
1137- f"logprobs must be between 1 and 10 but got { self .logprobs :,} "
1151+ f"top_logprobs must be between 1 and 10 but got { self .top_logprobs :,} "
11381152 )
11391153
11401154 if (
@@ -1174,7 +1188,7 @@ def generate_response(
11741188 prompt_token_count : Optional [int ] = None ,
11751189 completion_token_count : Optional [int ] = None ,
11761190 token_logprobs : Optional [List [float ]] = None ,
1177- top_tokens : Optional [List [Dict [int , float ]]] = None ,
1191+ top_tokens : Optional [List [Tuple [ Dict [str , Any ] ]]] = None ,
11781192 tokens : Optional [List [int ]] = None ,
11791193 tool_calls : Optional [List [str ]] = None ,
11801194 reasoning_text : Optional [str ] = None ,
@@ -1193,8 +1207,8 @@ def generate_response(
11931207 response, used to populate the "usage" field (not used when stream).
11941208 token_logprobs (Optional[List[float]]): The log probabilities per token,
11951209 in token order.
1196- top_tokens (Optional[List[Dict[int, float ]]]): List of dictionaries mapping
1197- tokens to logprobs for the top N tokens at each token position.
1210+ top_tokens (Optional[List[Tuple[ Dict[str, Any ]]]] ): List of outputs from
1211+ _format_top_logprobs, giving info on the top N tokens at each token position.
11981212 tokens (Optional[List[int]]): List of tokens to return with logprobs structure
11991213 tool_calls (Optional[List[str]]): List of tool calls.
12001214 reasoning_text (Optional[str]): The reasoning text generated by the model.
@@ -1222,11 +1236,17 @@ def generate_response(
12221236 ],
12231237 }
12241238
1225- if token_logprobs or top_logprobs or tokens :
1239+ if top_logprobs :
1240+ response ["choices" ][0 ]["logprobs" ] = {
1241+ "content" : [
1242+ dict (i [0 ], top_logprobs = i ) if i else {} for i in top_logprobs
1243+ ]
1244+ }
1245+ elif token_logprobs :
12261246 response ["choices" ][0 ]["logprobs" ] = {
1227- "token_logprobs " : token_logprobs ,
1228- "top_logprobs" : top_logprobs ,
1229- "tokens" : tokens ,
1247+ "content " : [
1248+ dict ( id = i , logprob = g ) for i , g in zip ( tokens , token_logprobs )
1249+ ]
12301250 }
12311251
12321252 if not self .stream :
@@ -1294,7 +1314,9 @@ def handle_completion(self, request: CompletionRequest, stop_words: List[str]):
12941314 max_tokens = self .max_tokens ,
12951315 num_draft_tokens = self .num_draft_tokens ,
12961316 logprobs = self .logprobs ,
1317+ top_logprobs = self .top_logprobs ,
12971318 seed = self .seed ,
1319+ chat_template_kwargs = self .chat_template_kwargs ,
12981320 )
12991321
13001322 # Create keepalive callback to send SSE comments during long prompt processing
@@ -1323,7 +1345,7 @@ def keepalive_callback(processed_tokens, total_tokens):
13231345 except Exception as e :
13241346 self ._set_completion_headers (404 )
13251347 self .end_headers ()
1326- self .wfile .write (( f"{ e } " ).encode ())
1348+ self .wfile .write (json . dumps ({ "error" : f"{ e } " } ).encode ())
13271349 return
13281350
13291351 # Prepare the headers
@@ -1420,10 +1442,11 @@ def parse_tools(tool_calls):
14201442
14211443 # Save the token and its logprob
14221444 tokens .append (gen .token )
1423- token_logprobs .append (gen .logprob )
1445+ if args .logprobs :
1446+ token_logprobs .append (gen .logprob )
14241447
14251448 # If requested save the k top logprobs
1426- if gen . top_tokens is not None :
1449+ if args . top_logprobs > 0 :
14271450 top_tokens .append (gen .top_tokens )
14281451
14291452 # Check if we should stop early
0 commit comments