From 8b85ba1a333d333f8b45f592d95f76f06f720107 Mon Sep 17 00:00:00 2001 From: Tai An Date: Fri, 17 Jul 2026 19:46:02 -0700 Subject: [PATCH] fix(api): don't iterate a string banned_tokens character by character (#2333) (#2336) * fix(api): don't iterate a string banned_tokens character by character (#2333) banned_tokens/banned_strings are consumed as a list of substrings. When a caller supplies a bare string instead, `banned_tokens[:ban_token_max]` slices it and `for tok in banned_tokens` walks it one character at a time, so every letter in the value becomes its own banned substring. Banning common letters like "e" makes generation collapse into garbage, with no error to point at the cause. Coerce a string value into a single-element list, matching how the OpenAI `stop` parameter is already normalized in transform_genparams. * fix(api): parse a JSON-array string in banned_tokens instead of inerting it Per review: coercing the string form to a single-element list left the reported gendefaults case a seemingly-effective no-op. Parse a JSON array supplied as a string so the reported config bans "exclude" as intended, and keep the bare-string form ("exclude") working as a single substring ban. --------- Co-authored-by: Anai-Guo --- koboldcpp.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/koboldcpp.py b/koboldcpp.py index 0c75d878c..92c1797f4 100644 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -2077,6 +2077,23 @@ def load_model(model_filename): ret = handle.load_model(inputs) return ret +def coerce_ban_list(value): + # banned tokens/strings are consumed as a list of substrings. A bare string satisfies + # every operation there, but iterates character by character, banning single letters. + if not value: + return [] + if not isinstance(value, str): + return value + stripped = value.strip() + if stripped.startswith('[') and stripped.endswith(']'): # a JSON array sent as a string, e.g. through gendefaults + try: + parsed = json.loads(stripped) + if isinstance(parsed, list): + return [str(item) for item in parsed] + except json.JSONDecodeError: + pass + return [value] + def generate(genparams, stream_flag=False): global maxctx, args, currentusergenkey, totalgens, pendingabortkey default_adapter = {} if chatcompl_adapter is None else chatcompl_adapter @@ -2141,8 +2158,8 @@ def generate(genparams, stream_flag=False): min_p = 0.002 logit_biases = genparams.get('logit_bias', {}) render_special = genparams.get('render_special', False) - banned_strings = genparams.get('banned_strings', []) # SillyTavern uses that name - banned_tokens = genparams.get('banned_tokens', banned_strings) + banned_strings = coerce_ban_list(genparams.get('banned_strings', [])) # SillyTavern uses that name + banned_tokens = coerce_ban_list(genparams.get('banned_tokens', banned_strings)) bypass_eos_token = genparams.get('bypass_eos', False) tool_call_fix = genparams.get('using_openai_tools', False) custom_token_bans = genparams.get('custom_token_bans', '')