Skip to content

Add FunctionCallingLogitsProcessor for tool call support#1910

Open
Youssefares wants to merge 6 commits into
dottxt-ai:mainfrom
Youssefares:function-calling
Open

Add FunctionCallingLogitsProcessor for tool call support#1910
Youssefares wants to merge 6 commits into
dottxt-ai:mainfrom
Youssefares:function-calling

Conversation

@Youssefares

Copy link
Copy Markdown
Contributor

Summary

Adds a FunctionCallingLogitsProcessor that switches between free generation and constrained JSON generation around tool call delimiters, implementing the v0 design from #1626.

Refactors get_logits_processor to reuse for compiling tools_type.

How it works

Each sequence in the batch independently transitions through three phases:

  1. PASSTHROUGH - logits returned unmodified. Scans for the open delimiter token.
  2. ARGUMENTS - delegates to an inner OutlinesCoreLogitsProcessor that constrains generation to the JSON schema. Checks guide.is_finished() after each step.
  3. CLOSING - masks all tokens to -inf except the close delimiter.

After closing, returns to PASSTHROUGH — supports multiple tool calls per generation.

Implementation details

  • Compiles the JSON schema into an FSM Index once at construction. Index 1:* Batch but Guide 1:1 Batch.
  • Slices input_ids so the inner processor only sees tokens generated since entering ARGUMENTS, not the passthrough prefix or open delimiter.
  • Cross-library token masking via _force_single_token_row handles numpy, torch, and MLX (MLX arrays are immutable, so standard item assignment doesn't work).

Tests

  • 20 unit tests parametrized across numpy, torch, and MLX
  • 1 integration test with a real OutlinesCoreLogitsProcessor, GPT-2 tokenizer, and simulated generation loop: verifies the output between delimiters is valid JSON matching the schema.

fix #1626

Youssef Fares added 3 commits July 9, 2026 16:29
Adds a logits processor that switches between free generation and
constrained JSON generation around tool call delimiters (<tool_call>
and </tool_call>). Supports batched generation with per-sequence
phase tracking and multiple sequential tool calls.

fix dottxt-ai#1626
Pulls the output-type-to-logits-processor dispatch out of
SteerableGenerator.__init__ into a get_logits_processor helper in the
backends module, next to the type-specific get_*_logits_processor
functions. FunctionCallingLogitsProcessor now reuses this helper
instead of hand-rolling JSON schema conversion, so it inherits support
for dataclasses, typed dicts, and nested types.

Also renames the tool schema parameter to tools_type.
Mirrors the OutlinesCoreLogitsProcessor dispatch: pick the
library-specific masking function once at construction based on
tensor_library_name, rather than importing torch/mlx and running
isinstance checks on every CLOSING step. The drop mask depends only
on the close token and vocab size, so it is built once and cached.
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

📚 Documentation preview: https://dottxt-ai.github.io/outlines/pr-preview/pr-1910/

Preview updates automatically with each commit.

Youssef Fares added 3 commits July 10, 2026 10:04
Annotate _inner_processors as List[Optional[...]] and _close_drop_mask
as Optional[np.ndarray], add an assert for type narrowing after the
inner processor is created, and mark TensorType indexing/masking ops
with type: ignore to match the convention in outlines_core.py.
- Drop the close-mask cache (negligible benefit, removed a branch)
- Turn the unreachable `elif CLOSING` into `else`
- Mark the MLX masking path `# pragma: no cover` (matches outlines_core)
- Add tests for the missing-delimiter error, prototype reuse on
  non-outlines_core backends, and the no-`_guides` path
@RobinPicard

Copy link
Copy Markdown
Contributor

Hi @Youssefares! Thanks for looking into this topic. I think it's something for which we still need to do a bit more thinking to ensure we use an interface that would work for various tool calling format though. We want to avoid the situation in which we have to break the interface first introduced because it's not exhaustive enough.

@Youssefares

Copy link
Copy Markdown
Contributor Author

Hi @Youssefares! Thanks for looking into this topic. I think it's something for which we still need to do a bit more thinking to ensure we use an interface that would work for various tool calling format though. We want to avoid the situation in which we have to break the interface first introduced because it's not exhaustive enough.

Thanks, Robin. Makes sense! Would it be useful to look into a few formats we're looking to support? and spec'ing out the different possible interfaces. If so, what formats are they? I can look into it and update the issue with details if you tell me 3-4 names that you believe will give us good breadth 🙌

@RobinPicard

RobinPicard commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Thanks for proposing! We ideally want an approach that could accommodate the best open source models such as those from DeepSeek, Kimi, Z.ai and Qwen. They are annoying to work with as some use strange formats with XML-like key-value tags system, multiple nested tags... But in general as a disclaimer I cannot tell you when we'll actually merge anything on that topic. Experimenting is good though, so if you feel like opening draft PRs to showcase different approaches, that would be valuable.

@Youssefares

Copy link
Copy Markdown
Contributor Author

Np! Appreciate the openness. Sounds good! As a way to leave this in a semi-mergeable state, I will have look at the open source model formats and will update the corresponding issue (if not the PR too). If simple enough I will work into this PR, then we can merge at leisure 🙌

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.

Add function calling and MCP support

2 participants