Skip to content

Worker rejects tools and tool_choice in chat completions #71

@andreypfau

Description

@andreypfau

Bug

POST /v1/chat/completions with OpenAI-format tools or tool_choice is rejected by the validator before reaching the model:

code=621 msg=worker: invalid http request:  has unknown field 'tools'
code=621 msg=worker: invalid http request:  has unknown field 'tool_choice'

In runners/helpers/ValidateRequest.cpp:1138-1144 both field handlers are written but gated behind if (false) { ... }, so check_unprocessed_fields (line 463) treats them as unknown and rejects.

Second issue: even after the validator accepts the fields, sglang in spec/spec-worker/cocoon-sglang.service:8 is launched without --tool-call-parser, so it would return function calls as plain text inside content instead of as a structured message.tool_calls array. For Qwen3 use --tool-call-parser qwen3 (or hermes as fallback).

Reproduce

No networking needed — validate_client_request (runners/helpers/ValidateRequest.h:47) is the same code path the worker uses, callable directly:

#include "runners/helpers/ValidateRequest.h"
#include <iostream>

int main() {
  std::string body = R"({
    "model": "Qwen/Qwen3-32B",
    "messages": [{"role":"user","content":"hi"}],
    "tools": [{"type":"function","function":{"name":"get_weather","parameters":{"type":"object"}}}],
    "tool_choice": "auto"
  })";
  auto r = cocoon::validate_client_request(
      "/v1/chat/completions", "application/json", body,
      nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
  std::cout << (r.is_error() ? r.move_as_error().to_string() : "OK") << "\n";
  return 0;
}

Today: [Error : 0 : protoviolation : /v1/chat/completions has unknown field 'tool_choice']. Drop tool_choice from the body, same shape with 'tools'.

Fix

--- a/runners/helpers/ValidateRequest.cpp
+++ b/runners/helpers/ValidateRequest.cpp
@@ -1138,11 +1138,9 @@ static td::Status process_chat_completions(Ctx &ctx) {
   TRY_STATUS(ctx.process_obj_field("temperature", false, process_double));
-  if (false) {
-    TRY_STATUS(ctx.process_obj_field("tool_choice", false, [](Ctx &ctx) { return td::Status::OK(); }));
-  }
-  if (false) {
-    TRY_STATUS(ctx.process_obj_field(
-        "tools", false, [](Ctx &ctx) { return ctx.process_array(false, [](Ctx &ctx) { return td::Status::OK(); }); }));
-  }
+  TRY_STATUS(ctx.process_obj_field("tool_choice", false, [](Ctx &ctx) { return td::Status::OK(); }));
+  TRY_STATUS(ctx.process_obj_field(
+      "tools", false, [](Ctx &ctx) { return ctx.process_array(false, [](Ctx &ctx) { return td::Status::OK(); }); }));
   TRY_STATUS(ctx.process_obj_field("top_logprobs", false, process_integer));
--- a/spec/spec-worker/cocoon-sglang.service
+++ b/spec/spec-worker/cocoon-sglang.service
@@ -8 +8 @@
-ExecStart=docker run ... --served-model-name $MODEL_NAME --enable-cache-report
+ExecStart=docker run ... --served-model-name $MODEL_NAME --enable-cache-report --tool-call-parser qwen3

Plus worker image rebuild + redeploy.

Verify

After the fix, a chat completion with tools should return a structured tool_calls array:

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_xxx",
        "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions