Skip to content

fix(harness): guarantee a resolvable user turn for non-native-tool models - #86

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/5291-nonnative-tools-user-turn
Aug 2, 2026
Merged

fix(harness): guarantee a resolvable user turn for non-native-tool models#86
senamakel merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/5291-nonnative-tools-user-turn

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Contributor

Summary

A model whose profile reports no native tool calling is driven through its own Jinja chat template by the serving runtime (LM Studio, llama.cpp, Ollama), so the outgoing message list has to satisfy that template — not just the wire schema. Several widely used templates hard-require a locatable user query. Qwen 3's raises outright:

{%- if ns.multi_step_tool %}
    {{- raise_exception('No user query found in messages.') }}

A prompt-guided tool loop reaches that state legitimately: once the real user turn has aged out of the window — summarization, a resumed transcript, a task carried entirely by the system prompt — every remaining non-system turn is an assistant continuation or a folded tool result, and the runtime aborts with a 400 before the model is ever called.

Reported downstream as tinyhumansai/openhuman#5291 (LM Studio + qwen/qwen3.5-9b, supports_native_tools=false, 80 tools, both retries dead).

API Or Behavior Changes

  • New public helper harness::tool::ensure_resolvable_user_turn(&[Message]) -> Vec<Message> (re-exported via the existing pub use prompt::*). Guarantees the list contains a user turn a chat template can resolve, inserting one after any leading system turns only when none is present; a list that already has a real user turn is returned unchanged.
  • Applied by the OpenAI-compatible adapter (translate_request_with) for no-native-tools profiles, after the existing tool-result coalescing.
  • Native-tool models are untouched — they are served through the provider's own tool protocol, not a template with this guard. Pinned by a test in both directions.

Two judgment calls worth reviewer attention:

  1. A folded [Tool results] turn does not count as a resolvable query. The model requested those itself, and Qwen's template makes the same distinction (it skips user turns that are wholly a tool response). The marker string is now a shared const so coalesce_prompt_tool_results and the resolvability check cannot drift.
  2. Scoped to !profile.tool_calling, not to prompt_guided_tools. The guard is a property of the template, not of whether tools were sent — so a no-native-tools model with an empty tool list and no user turn is covered by the same normalization.

A non-text user turn (JSON, an image) does count as resolvable — an image-only turn is real user input and must not be displaced.

Tests

Six unit tests in src/harness/tool/prompt_test.rs (real query left alone · insertion after leading system turns · folded tool results do not count · blank turn ignored · image-only turn accepted · insertion at index 0 when there is no system turn) and two adapter tests in src/harness/providers/openai/test.rs (prompt-guided wire body carries exactly one resolvable user query in system → user → assistant order; native-tool model's messages untouched).

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo clippy --all-targets --all-features -- -D warnings — not run
  • cargo build --all-targets — not run separately (covered by the test build)
  • cargo build --all-targets --all-features — not run
  • cargo test (--lib, 1232 passed / 0 failed)
  • cargo test --all-features — not run

Documentation

docs/modules/harness/tool.md gains a Prompt-Guided Message Shape section documenting both normalization helpers, why the template (not the wire schema) is the constraint for these models, and why a folded tool result is not a query.

…dels

A model whose profile reports no native tool calling is driven through
its own Jinja chat template by the serving runtime (LM Studio,
llama.cpp, Ollama), so the outgoing message list has to satisfy that
template and not just the wire schema. Several widely used templates
hard-require a locatable user query; Qwen 3's raises outright:

    {%- if ns.multi_step_tool %}
        {{- raise_exception('No user query found in messages.') }}

A prompt-guided tool loop reaches that state legitimately. Once the real
user turn has aged out of the window — summarization, a resumed
transcript, a task carried entirely by the system prompt — every
remaining non-system turn is an assistant continuation or a folded tool
result, and the runtime aborts the request with a 400 before the model
is ever called.

Add `ensure_resolvable_user_turn`, applied by the OpenAI-compatible
adapter for no-native-tools profiles after the existing tool-result
coalescing. It inserts a user turn after any leading system turns only
when the list has none. A folded `[Tool results]` turn does not count as
a query — the model requested those itself, and Qwen's template makes
the same distinction — while a non-text turn (JSON, an image) does.
Native-tool models are served through the provider's tool protocol
rather than a template with this guard, so their message list is
untouched.

Refs tinyhumansai/openhuman#5291

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

M3gA-Mind has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9eaf7937-d5e5-4584-95b3-c4eba062d91f

📥 Commits

Reviewing files that changed from the base of the PR and between 801a77c and 59b941d.

📒 Files selected for processing (5)
  • docs/modules/harness/tool.md
  • src/harness/providers/openai/test.rs
  • src/harness/providers/openai/transport.rs
  • src/harness/tool/prompt.rs
  • src/harness/tool/prompt_test.rs

Comment @coderabbitai help to get the list of available commands.

@M3gA-Mind

Copy link
Copy Markdown
Contributor Author

Cross-link: the OpenHuman-side half of tinyhumansai/openhuman#5291 is tinyhumansai/openhuman#5309 — it reclassifies the template-parse 400 so the user is told the chat template rejected the request, instead of "your provider rejected the model or temperature setting".

The two are independent: #5309 does not bump the vendor/tinyagents pin, and this PR does not depend on it. This one removes the message shape that trips the template; that one makes the failure legible while any other template guard still fires.

@senamakel
senamakel merged commit 51752da into tinyhumansai:main Aug 2, 2026
2 checks passed
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.

2 participants