Skip to content

Worker rejects tools and tool_choice in chat completions #71

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions