Skip to content

fix: enforce OpenAI tool-choice contract - #650

Draft
waw2637 wants to merge 7 commits into
ROCm:mainfrom
waw2637:fix/openai-tool-choice-contract
Draft

fix: enforce OpenAI tool-choice contract#650
waw2637 wants to merge 7 commits into
ROCm:mainfrom
waw2637:fix/openai-tool-choice-contract

Conversation

@waw2637

@waw2637 waw2637 commented Aug 4, 2026

Copy link
Copy Markdown

Summary

  • enforce Chat Completions tool_choice modes (none, auto, required, named function, and allowed_tools) and parallel_tool_calls
  • validate function definitions and generated calls before returning them, failing closed and clearing parser/KV/prompt-cache state after rejected generations
  • buffer tool-call streams for validation and honor stream_options.include_usage
  • wire the contract unit suite into root CMake/CTest and Debian PR builds
  • document supported behavior, structured failures, streaming semantics, safe dispatch, and explicit limitations

Closes #649

Why

FastFlowLM 0.9.46 could return HTTP 200 with finish_reason: "tool_calls" even when a truncated model response produced an empty or undeclared function name or malformed arguments. This was reproduced across Gemma 4, Qwen 3, and Qwen 3.5. Clients then attempted to dispatch calls that were not usable.

The REST boundary now treats the model/parser output as untrusted and validates it against the request contract before exposing it to clients.

Supported contract

  • function tools on /v1/chat/completions
  • tool_choice: none, auto, required, named function, and allowed_tools
  • serial or parallel calls according to parallel_tool_calls
  • JSON-encoded object arguments and declared function names
  • buffered SSE tool responses with optional final usage chunk

Explicit limitations

  • strict: true is rejected because constrained JSON Schema decoding is not implemented
  • custom and hosted tools are not supported
  • Responses API tools are not supported
  • applications remain responsible for full JSON Schema validation, authorization, and safe function execution

Verification

Build and unit tests

  • configured the root project with the Ubuntu 26.04 FastFlowLM build environment
  • built the full flm target successfully (96 build steps)
  • ran CTest: openai_tool_policy_contract passed
  • verified the installed test binary matches the built artifact by SHA-256

Live NPU contract matrix

All models were loaded and tested serially with the binary built from 11c4081.

Test Gemma 4 E4B (64K) Gemma 4 E2B (8K) Qwen 3 Instruct 4B (8K) Qwen 3.5 2B (8K)
Invalid request fields return HTTP 400 Pass Pass Pass Pass
tool_choice: none Pass Pass Pass Pass
tool_choice: required Pass Pass Pass Pass
Named function choice Pass Pass Pass Pass
allowed_tools required subset Pass Pass Pass Pass
Truncated call fails closed, then recovers Pass Pass Pass Pass
Multi-turn chained calls Pass Pass Pass Pass
SSE usage absent/opt-in shapes Pass Pass Pass Pass
Autonomous skill/read/write workflow Pass Not run Not run Not run

The recovery test deliberately truncates a required call with max_tokens: 1, expects a structured HTTP 500 model error, and immediately sends a valid required call to verify that rejected state is not reused.

Related reports

These can improve how often a model generates a valid call, while this PR protects the API boundary when it does not.

Review requested

  • general correctness and regression review
  • API/architecture review of Chat Completions compatibility and failure semantics
  • security review of fail-closed behavior and client execution guidance

This PR is intentionally draft until independent review confirms the contract and test coverage.

@waw2637
waw2637 marked this pull request as ready for review August 27, 2026 19:16
Copilot AI lite review requested due to automatic review settings August 27, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens FastFlowLM’s OpenAI-compatible Chat Completions boundary by enforcing tool_choice / allowed_tools / parallel_tool_calls, validating emitted tool calls before returning them to clients, and buffering tool-call streaming to fail closed on malformed generations.

Changes:

  • Add a centralized tool-policy parser + response validator (openai_tool_policy.hpp) and integrate it into /v1/chat/completions handling (including buffered SSE streaming with optional usage chunk).
  • Add a focused CTest unit suite plus a manual live-server Python contract harness for tool policy / SSE behavior.
  • Wire the contract test into CMake/CTest and Debian builds, and document the supported contract and failure semantics.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/include/openai_tool_policy.hpp Implements tool policy parsing, tool-call validation, and buffered streaming chunk builder.
src/server/rest_handler.cpp Applies tool policy to requests, validates model tool calls, buffers tool-call streaming, and clears state on rejected generations.
src/server/server.cpp Maps structured error code values to HTTP 400/500 for OpenAI-style error responses.
src/test/openai_tool_policy/test.cpp Adds CTest unit coverage for policy parsing, validation, and buffered SSE chunk shape.
src/test/openai_tool_policy/CMakeLists.txt Builds the new unit test executable and registers it with CTest.
src/test/openai_tool_policy/test_server_contract.py Adds a manual live-server contract harness for HTTP + SSE behavior (including recovery).
src/test/openai_tool_policy/README.md Documents how to run the unit test and the manual live-server harness.
src/CMakeLists.txt Enables CTest and adds the new openai_tool_policy test directory when BUILD_TESTING is on.
docs/docs/instructions/server/tool_calling.md Documents the supported OpenAI tool-selection contract, buffered streaming semantics, and safe dispatch guidance.
debian/rules Runs ctest during Debian builds to ensure the contract test is executed in packaging CI.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/include/openai_tool_policy.hpp
Comment thread src/test/openai_tool_policy/test.cpp
waw2637 and others added 2 commits August 27, 2026 15:27
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This fixes a missing Missing tool_calls[].index  API compatibility bug

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

meta_info.load_duration = (uint64_t)time_utils::duration_ns(load_start_time, load_end_time).first;
meta_info.max_prefill_len = this->prefill_chunk_len;
if (stream){
if (stream && !tool_policy.requires_buffered_validation()){
meta_info.load_duration = (uint64_t)time_utils::duration_ns(load_start_time, load_end_time).first;
meta_info.max_prefill_len = this->prefill_chunk_len;
if (stream){
if (stream && !tool_policy.requires_buffered_validation()){

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really not a good way to fix this. I am working on a better fix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to adapt my 1.0.1 fix -- got it working with a full context (128000) full of turns and tools in OpenCode. I also updated the chat template in my models and that helped with it some. Works with pi as well.

It shouldn't be super hard!

@waw2637 waw2637 Aug 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So i looked at your fix. It does fix a very similar kind of issue but in an area I would consider a separate boundary and area of concern making these patches not so similar. The above bug that is noted by the copilot really is addressing the streaming/non streaming pairty behavior that is expected in openai.

There is a distinct lack of boundary responsibility distinction in this library. My goal in the patch was also so move this closer to an architecture rather than collections of patches. Roughly with the goal of creating sensible boundaries of responsibility.

Something like this

HTTP request

OpenAI request

Validate OpenAI contract

Convert to canonical conversation

Adapt canonical conversation for model/template

Inference

Parse model-specific output

Canonical assistant result

Enforce requested tool policy

Convert to OpenAI response/chunks

HTTP JSON or SSE response

Your change would live near and handle boundary in canonical conversation to template layer. This patch really is trying to build out a proper openai API boundary.

I would love help/to help with building these layers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me! Standards and best practices are good.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Atomic-Germ This project needs a large refactor to be anywhere as close to usable, stable, maintainable and I kinda wonder if anyone is home on this thing too.

@waw2637
waw2637 marked this pull request as draft August 27, 2026 19:55
@Javinator9889

Copy link
Copy Markdown

@waw2637 I'm really interested in this functionality, mainly because OpenCode is not receiving correctly the context information so it's missing compactation every single time, and not efficiently managing contexts. If you need any help, ping me

@waw2637

waw2637 commented Aug 28, 2026

Copy link
Copy Markdown
Author

@Javinator9889 does your issue match the issue linked to this PR #649. Or is it a new behavior. From your brief description it is possible that my code could help, but it does not per se fix tool handling, and compaction is kinda another area all together that i have not gotten into at all with fastflowlm.

A lot can still go wrong other layers giving you trash output.

Convert to canonical conversation

Adapt canonical conversation for model/template

Inference

Parse model-specific output

Canonical assistant result

Enforce requested tool policy

The code i have here simply makes sure that the api contract for OpenAI is enforced correctly. It doesn't recheck all earlier layers parsing and handling.

@Javinator9889

Copy link
Copy Markdown

Yeah, it's the missing piece for OpenCode AFAIK. The stream.include_usage option is not honored so OpenCode sees a new, empty context per invocation, hence never triggering compaction or similar behaviors @waw2637

So your PR, as it enforces a correct OPEN API endpoint should fix that

@waw2637

waw2637 commented Aug 28, 2026

Copy link
Copy Markdown
Author

@Javinator9889 It does do that so what can I do to help you. My fork (is level with main and contains my patch) compiles under Ubuntu. I think I am going to collect some of the prs and patches languishing and pull them into my fork (not ideal but if others find it useful).

@Javinator9889

Copy link
Copy Markdown

@Javinator9889 It does do that so what can I do to help you. My fork (is level with main and contains my patch) compiles under Ubuntu. I think I am going to collect some of the prs and patches languishing and pull them into my fork (not ideal but if others find it useful).

What's the deal with merging upstream? No maintainers, reviewers?

@Atomic-Germ

Copy link
Copy Markdown

@Javinator9889 It does do that so what can I do to help you. My fork (is level with main and contains my patch) compiles under Ubuntu. I think I am going to collect some of the prs and patches languishing and pull them into my fork (not ideal but if others find it useful).

What's the deal with merging upstream? No maintainers, reviewers?

Exactly.

They basically aren't here, and it's only partly open source, just enough for AMD to celebrate themselves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: tool_choice is not enforced and malformed tool calls are returned as successful responses

4 participants