fix(serve): a non-object tool_choice.function answered 500 "engine failed" - #1598
Merged
JustVugg merged 1 commit intoSep 18, 2026
Merged
Conversation
…iled"
generation_options() already refuses a `tool_choice` function object that
carries no name, with a 400. It never got the chance: the line above reads
`(choice.get("function") or {}).get("name")`, so writing the name where the
object goes -- {"type": "function", "function": "search"} instead of
{"function": {"name": "search"}} -- raises AttributeError, and do_POST's
catch-all answers HTTP 500 "The colibri engine failed to process the request."
OpenAI SDKs retry a 5xx, against an engine that was never asked anything.
The five renderers that read the forced tool (GLM-5.2, GLM-5.3, Kimi, DeepSeek
V4 and V4.1) carry the same expression, and on /v1/chat/completions the prompt
is rendered before generation_options runs, so they crash first; /v1/completions
reaches the validator directly and crashed there. Both endpoints, every arch.
Read the member, then check it -- the same shape as the json_schema fix
(JustVugg#1587) -- through one helper, so the existing 400 covers all six call sites.
The legacy {"type": "function", "name": ...} spelling and a well-formed
function object are unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JustVugg
pushed a commit
that referenced
this pull request
Sep 23, 2026
{"type": "function", "function": "search"} on tools[] is the same
client slip #1598 covered on tool_choice. generation_options already
has 400 "Tool function must be an object". The GLM and DeepSeek
declaration blocks did fn.items() first, so do_POST answered 500
"engine failed" for a request the engine never saw.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happens
[api] 127.0.0.1 - request failed: 'str' object has no attribute 'get'Writing the name where the object goes is an ordinary client slip, and the
server already has an answer for it --
generation_options()raises400 "tool_choice function object must include a name."two lines furtherdown. It never runs, because the read above it is
which raises
AttributeErroras soon asfunctionis not an object. Thatreaches
do_POST's catch-all, and the request comes back as a 5xx that OpenAISDKs retry -- against an engine that was never asked anything.
Reach
The same expression is copied into the five renderers that read the forced
tool, and on
/v1/chat/completionsthe prompt is rendered beforegeneration_optionsruns, so those crash first./v1/completionshas norenderer in front and reached the validator's own copy.
generation_options)/v1/completions, any archgeneration_options)/v1/messagesis unaffected:anthropic_tools()builds its owntool_choiceand already validates it.
The change
Read the member, then check it -- the same shape as the
json_schemafix(#1587) -- through one small helper, so the six call sites that share the
expression share the guard and the existing 400 is what runs. The legacy
{"type": "function", "name": "search"}spelling still forces the tool, and awell-formed function object renders byte-for-byte as before; both are asserted.
Verification
Full module and neighbours, on Windows (Python 3.11):
The
HTTPTestcases drive a realAPIServerover a socket, so what is assertedis the status the client sees.
🤖 Generated with Claude Code