Repository navigation
feat(huggingface-transformers): constrain forced tool calls with a grammar - #1031
Conversation
…ammar When toolChoice names a tool or is "required", decode the <tool_call> body under StructuredOutputProcessor so the call is valid JSON for the tool's input schema. Applies to JSON-in-tag families (qwen, qwen2, qwen3, hermes); auto turns, other families, post-tool-result turns and schemas the engine rejects stay unconstrained. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
|
The failing calls are No fix exists yet that I can port. Build, lint and typecheck are green on this head. I'll re-run the failed job once, when the rest of the run finishes. Generated by Claude Code |
…ng is unroutable Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
There was a problem hiding this comment.
Review progress ██████████ 4/4 files
Comment — found 1 issue(s) at 082c46d.
Actionable comment posted: 1
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1031: https://github.com/workglow-dev/libs/pull/1031 (head branch `claude/hft-constrained-tool-calling`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@packages/test/src/test/ai-provider-api/HFI_Generic.integration.test.ts` (line 25, Important):
`TOOL_CALLING_ROUTABLE` is hard-coded to `false`, so `RUN` is always false even when `HF_TOKEN` is set and `runAiProviderConformance` skips its entire suite. That also disables the embedding finite-vector check for the separate `EMBED_MODEL_ID` and registry/capability checks, although those paths do not use the unroutable tool-call model. Preserve the checks that remain runnable while skipping only the failing tool-call coverage.
|
That is a billing state on the live Gemini API, not something this PR's diff ( Generated by Claude Code |
…n deepinfra The suite pinned Llama 3.3 70B to the together router, which no longer serves it, so every tool-call run failed. Use a model and provider the router lists as live with tool support, and re-enable the whole suite. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
|
Update on
None of these touch Generated by Claude Code |
…n deepinfra Ported from libs#1031 (6cf4074), as that change leaves the file. The suite pinned Llama 3.3 70B to the together router, which no longer serves it, so every tool-call run failed. The net change from main is the model and provider swap; #1031's interim skip is not carried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
- required with several tools: name is an enum with per-tool if/then, so a wrong name is refused token by token instead of at the closing brace - embed tool schemas for the engine: drop x-* annotations, re-root local $refs - cache envelopes by content so the engine's per-schema mask cache is reused - read history and tools from the checkpoint prefix as well as the call - fall back to free decoding when the grammar attempt throws mid-generation - skip the constraint when a generation prefix already exists - tests drive the real grammar; fix import order Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
…ed-call tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
There was a problem hiding this comment.
Review progress ██████████ 4/4 files
Request changes — found 1 issue(s) at 1b61bd3.
Actionable comment posted: 1
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1031: https://github.com/workglow-dev/libs/pull/1031 (head branch `claude/hft-constrained-tool-calling`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@providers/huggingface-transformers/src/ai/common/HFT_ConstrainedToolCall.ts` (line 226, Blocker):
`candidateTools` can select tools from `prefix.tools` when the checkpoint consumer's `input.tools` is empty, but `HFT_ToolCalling` later validates parsed calls using only `input.tools` (`filterValidToolCalls` at line 690). A checkpoint turn relying on its stored tools can therefore decode a valid constrained call and then silently drop it, so no `toolCalls` event reaches the caller. Use the same effective tool list for post-parse validation.
…oint prefix's tools A checkpoint turn builds its prompt and its constraint from the prefix's tools, but the decoded call was filtered against the call's own list only, so a call to a prefix-only tool was dropped. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
There was a problem hiding this comment.
Review progress ██████████ 4/4 files
Request changes — found 2 issue(s) at d08deac.
Actionable comments posted: 2
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1031: https://github.com/workglow-dev/libs/pull/1031 (head branch `claude/hft-constrained-tool-calling`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@providers/huggingface-transformers/src/ai/common/HFT_ToolCalling.ts` (line 573, Important):
`createStreamingTextStreamer` creates a fresh usage reporter for each `decode()` attempt. If constrained generation emits usage and then throws, the fallback reporter starts again at zero (including emitting the same prompt count with output `0`); usage snapshots replace one another in `StreamEventAccumulator`, so the final reported usage omits the failed attempt's tokens and can move backwards. Preserve cumulative counters across both attempts, including the repeated prompt work.
Additional findings outside changed lines:
1. In `@providers/huggingface-transformers/src/ai/common/HFT_ConstrainedToolCall.ts` (line 226, Blocker):
Validate calls against checkpoint tools too
`candidateTools` can select tools from `prefix.tools` when the checkpoint consumer's `input.tools` is empty, but `HFT_ToolCalling` later validates parsed calls using only `input.tools` (`filterValidToolCalls` at line 690). A checkpoint turn relying on its stored tools can therefore decode a valid constrained call and then silently drop it, so no `toolCalls` event reaches the caller. Use the same effective tool list for post-parse validation.
Still open from earlier reviews (1) — the lines they point at have not changed:
- 🔴 Blocker · Validate calls against checkpoint tools too ·
providers/huggingface-transformers/src/ai/common/HFT_ConstrainedToolCall.tsline 226 · from1b61bd3
candidateToolscan select tools fromprefix.toolswhen the checkpoint consumer'sinput.toolsis empty, butHFT_ToolCallinglater validates parsed calls using onlyinput.tools(filterValidToolCallsat line 690). A checkpoint turn relying on its stored tools can therefore decode a valid constrained call and then silently drop it, so notoolCallsevent reaches the caller. Use the same effective tool list for post-parse validation.
Usage snapshots replace one another, and each decode starts its own count, so a retry after a failed constrained attempt moved the totals backwards and dropped the failed attempt's tokens. Flush the failed attempt's throttled tail and add what it reported to the retry's snapshots. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG
There was a problem hiding this comment.
Review progress ██████████ 4/4 files
Request changes — found 1 issue(s) outside changed lines at ba533e6.
Actionable comment posted: 1
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1031: https://github.com/workglow-dev/libs/pull/1031 (head branch `claude/hft-constrained-tool-calling`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@providers/huggingface-transformers/src/ai/common/HFT_ConstrainedToolCall.ts` (line 226, Blocker):
Validate calls against checkpoint tools too
`candidateTools` can select tools from `prefix.tools` when the checkpoint consumer's `input.tools` is empty, but `HFT_ToolCalling` later validates parsed calls using only `input.tools` (`filterValidToolCalls` at line 690). A checkpoint turn relying on its stored tools can therefore decode a valid constrained call and then silently drop it, so no `toolCalls` event reaches the caller. Use the same effective tool list for post-parse validation.
Still open from earlier reviews (1) — the lines they point at have not changed:
- 🔴 Blocker · Validate calls against checkpoint tools too ·
providers/huggingface-transformers/src/ai/common/HFT_ConstrainedToolCall.tsline 226 · from1b61bd3
candidateToolscan select tools fromprefix.toolswhen the checkpoint consumer'sinput.toolsis empty, butHFT_ToolCallinglater validates parsed calls using onlyinput.tools(filterValidToolCallsat line 690). A checkpoint turn relying on its stored tools can therefore decode a valid constrained call and then silently drop it, so notoolCallsevent reaches the caller. Use the same effective tool list for post-parse validation.
Summary
HFT tool calling parsed free-form output after the fact, so a small local model could emit a malformed or off-schema call. When a turn must end in one call, decode is now constrained with
@huggingface/transformers-structured-output(already a dependency, previously used only byHFT_StructuredGeneration).toolChoiceis a named tool or"required", the family isqwen,qwen2,qwen3orhermes(JSON inside<tool_call>), and no tool results are in the history (the call's own or its checkpoint prefix's).<tool_call>\nis appended to the prompt and aStructuredOutputProcessorconstrains the body to{"name": <const>, "arguments": <tool inputSchema>}. With several tools ("required") the name is anenumwith per-toolif/thenbranches, so a wrong name is refused token by token. Tool schemas are embedded for the engine (x-*annotations dropped, local$refs re-rooted), and envelopes are cached by content so the engine's mask cache is reused. The closing tag is written back before the family parser runs, and the body is not streamed as assistant text since it is the call."auto"(a grammar can't choose between answering and calling), other families, Qwen 3.5 (XML calls), a prompt ending inside an open<think>, and any tool schema the engine rejects.Also: the HF Inference conformance test pinned Llama 3.3 70B to a router provider that no longer serves it; it now uses Llama 3.1 8B on
deepinfra(live, with tool support on the HF router).New logic lives in
HFT_ConstrainedToolCall.ts, wired intoHFT_ToolCalling.ts.Test plan
HFT_ToolCalling.test.tsdrives the real grammar through a one-byte-per-id vocabulary (name refusal, missingarguments, checkpoint prefix, fallback, usage across the retry); HFT provider suite passesbun run build:types, oxlint and oxfmt cleanba533e6🤖 Generated with Claude Code
https://claude.ai/code/session_019frF5cRGhNB8N8QKDYY7dG