Merge pull request #437 from 4ny3l/fix-tool-calling-401

fix(serve): lift non-EOS stop tokens in C and fix redundant tools parsing in Python (#401)
This commit is contained in:
Vincenzo Fornaro
2026-07-20 17:07:34 +02:00
committed by GitHub
2 changed files with 37 additions and 5 deletions
+27 -5
View File
@@ -374,6 +374,27 @@ def generation_options(body, limit):
if body.get("n", 1) != 1:
raise APIError(400, "Colibri currently supports `n=1` only.", "n", "unsupported_value")
# `tools`/`functions` are handled by render_chat (declaration) + parse_tool_calls (output).
# Validate tools/functions structure early so malformed input fails with a clear error.
tools_raw = body.get("tools") or body.get("functions")
if tools_raw is not None:
if not isinstance(tools_raw, list):
raise APIError(400, "`tools` must be a non-empty array.", "tools", "invalid_value")
if not tools_raw:
raise APIError(400, "`tools` must be a non-empty array.", "tools", "invalid_value")
for idx, tool in enumerate(tools_raw):
if not isinstance(tool, dict):
raise APIError(400, f"Each tool must be an object, got {type(tool).__name__} at index {idx}.",
f"tools.{idx}", "invalid_value")
fn = tool.get("function", tool) if isinstance(tool, dict) else {}
if not isinstance(fn, dict):
raise APIError(400, f"Tool function must be an object at index {idx}.",
f"tools.{idx}.function", "invalid_value")
if not fn.get("name"):
raise APIError(400, f"Each tool must have a `name` at index {idx}.",
f"tools.{idx}.function.name", "invalid_value")
if not isinstance(fn["name"], str):
raise APIError(400, f"Tool `name` must be a string at index {idx}.",
f"tools.{idx}.function.name", "invalid_value")
choice = body.get("tool_choice")
if choice is not None:
if isinstance(choice, str):
@@ -863,7 +884,7 @@ class APIHandler(BaseHTTPRequestHandler):
except OSError:
pass
def generation(self, body, prompt, request_id, chat):
def generation(self, body, prompt, request_id, chat, tools=None, tool_choice=None):
# COLI_DEBUG tees the engine transaction to stderr: 1 = decoded output stream only,
# 2 = both sides (rendered prompt + output). render_chat already folds prior turns and
# tool results into `prompt`, so level 2 is the full conversation the engine saw.
@@ -875,8 +896,8 @@ class APIHandler(BaseHTTPRequestHandler):
sys.stderr.write(f"\n===== PROMPT [{request_id}] =====\n{prompt}\n===== OUTPUT [{request_id}] =====\n")
sys.stderr.flush()
maximum, temperature, top_p = generation_options(body, self.server.max_tokens)
tools = (body.get("tools") or body.get("functions") or None) if chat else None
if body.get("tool_choice") == "none":
# tools and tool_choice come from chat_completion() already processed/filtered
if chat and tool_choice == "none":
tools = None # client forbade tools: never surface tool_calls
cache_slot = body.get("cache_slot")
if (cache_slot is not None and
@@ -1078,9 +1099,10 @@ class APIHandler(BaseHTTPRequestHandler):
if not isinstance(enable_thinking, bool):
raise APIError(400, "`enable_thinking` must be a boolean.", "enable_thinking")
tools = body.get("tools") or body.get("functions") or None
tool_choice = body.get("tool_choice")
prompt = render_chat(body.get("messages"), enable_thinking, reasoning_effort, tools,
body.get("tool_choice"))
self.generation(body, prompt, request_id, True)
tool_choice)
self.generation(body, prompt, request_id, True, tools, tool_choice)
def completion(self, body, request_id):
prompt = body.get("prompt")
+10
View File
@@ -116,6 +116,16 @@ static void stops_arm_tok(const Cfg *c, int tok_eos, Tok *T){
int nsp = 0;
if (T) for (int id = 0; id < T->n_ids && g_nstop < 64; id++)
if (T->id_special[id] && !is_stop(id)) { g_stop[g_nstop++] = id; nsp++; }
/* #401: in serve mode keep ONLY <|endoftext|>. Role markers <|user|>/<|observation|>
* (config stops + tokenizer special set) are boundaries the Python server owns; as
* hard stops they cut generation the moment the model opens a <tool_call> block,
* because int4 argmax noise picks a stop-token ID over the correct '<' token. */
if (getenv("SERVE") && tok_eos >= 0) {
int kept = 0;
for (int i = 0; i < g_nstop; i++) if (g_stop[i] == tok_eos) g_stop[kept++] = g_stop[i];
if (kept < g_nstop) fprintf(stderr, "[stop] serve mode: filtered %d non-EOS stop tokens (tool-call safety, #401)\n", g_nstop - kept);
g_nstop = kept; nsp = 0;
}
fprintf(stderr, "[stop] %d stop tokens:", g_nstop);
for (int i = 0; i < g_nstop; i++) fprintf(stderr, " %d", g_stop[i]);
if (nsp) fprintf(stderr, " (%d from the tokenizer's special set)", nsp);