Skip to content

feat(ai): support constrained sampling#6341

Open
mitsuhiko wants to merge 8 commits into
mainfrom
grammar-constraints
Open

feat(ai): support constrained sampling#6341
mitsuhiko wants to merge 8 commits into
mainfrom
grammar-constraints

Conversation

@mitsuhiko

Copy link
Copy Markdown
Member

This adds an opt-in constrainedSampling config for tools so callers can request provider-side constrained tool input generation.

Basically tools can now ask the model provider to constrain tool arguments in two ways:

  • JSON-schema constrained sampling which is typically exposed as strict tool calling:
    • strict: "prefer" uses strict provider behavior when available and falls back otherwise.
    • strict: "require" fails clearly if the selected provider/model cannot honor it.
  • Grammar-constrained sampling for OpenAI tool calls, using OpenAI Lark or regex grammar variants.

Now there are some notes here that this PR does not address today. One of the motivations for prefer over true/false is that if you define too many tools to be strict, inference APIs can fail. The motivation here is that we can later (not done today) be more clever in how we deal with strictness. For now that's just over-engineering.

The second note is that only OpenAI today supports constraining for tool calls with custom grammar and it's unclear what other APIs will do in the future. Quite a few inference APIs support constraining on some grammar for regular responses for structured completions but that's not really applicable for pi today.

Because I do not want to design some feature around some OpenAI oddity the change here detains grammar-constrained tools to be like ordinary pi tools to callers: for custom grammar tools we use a normal object schema with one required string property. The idea here is that for providers without native grammar support we have a clear way to fall back to the normal JSON/function tool form. Custom providers and model configs can also declare whether they support strict tools or grammar tools, so pi does not assume that every OpenAI-compatible or Anthropic-compatible endpoint accepts these newer fields.

This all came out of the edit-tool investigation in #6278. Newer Claude models can sometimes produce semantically correct nested tool arguments with extra invented fields, especially in long agentic histories. To support apply_patch and other things from OpenAI the entire approach really only makes sense if you consider it as a free form tool so that's where the desire came in to support these grammars.

My experiments show that strict/constrained decoding can eliminate that class of failure and I want to put us into a position where we or users can use OpenAI specific custom tools as well.

This is what this looks like to the user for just constraining on JSON schema:

const strictJsonTool: Tool = {
  name: "edit_file",
  description: "Edit a file by replacing exact text.",
  parameters: Type.Object(
    {
      path: Type.String(),
      oldText: Type.String(),
      newText: Type.String(),
    },
    { additionalProperties: false },
  ),
  constrainedSampling: {
    type: "json_schema",
    strict: "prefer",
  },
};

And this is what it looks like with custom grammar for OpenAI only:

const larkGrammarTool: Tool = {
  name: "apply_patch",
  description: "Apply a unified diff patch.",
  parameters: Type.Object(
    {
      input: Type.String({
        description: "Unified diff patch text.",
      }),
    },
    { additionalProperties: false },
  ),
  constrainedSampling: {
    type: "grammar",
    variants: {
      openai_lark: String.raw`
start: /(.|\n)+/
`,
    },
  },
};

For the grammar case, pi still exposes { input: string } as the tool arguments. I also set up the grammar based sampling so that in case some other tool comes around in the future, you can define more than one grammar.

Fixes #6306

Add the constrained sampling API and provider plumbing for OpenAI and Anthropic tool calls, including grammar sampling variants and model capability metadata.
@mitsuhiko mitsuhiko added the to-discuss This ticket requires further discussion outside of triage. label Jul 5, 2026
chatTemplateKwargs: {},
zaiToolStream: false,
supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia,
supportsGrammarTools: provider === "openai",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe this field should be called supportsOpenAIGrammarTools.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yes

@badlogic

badlogic commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

how do we gate this to openai as a provider? how does this work via proxies?

mitsuhiko added 4 commits July 7, 2026 01:00
Grammar tool capability is now model metadata instead of a runtime
provider heuristic. The model generator stamps
compat.supportsGrammarTools on GPT-5+ models for endpoints verified or
documented to pass OpenAI custom tools through (OpenAI, OpenAI Codex,
Azure OpenAI, GitHub Copilot, opencode, Cloudflare AI Gateway). All
OpenAI-family APIs read only the compat flag and default to off, since
gateways that normalize tool schemas (e.g. OpenRouter) mangle custom
tools and OpenAI rejects them for pre-GPT-5 models. Azure and Codex
Responses now support grammar tools and carry OpenAIResponsesCompat.
# Conflicts:
#	packages/ai/src/api/openai-responses-shared.ts
@mitsuhiko

Copy link
Copy Markdown
Member Author

We enable grammar tools through per-model compat.supportsGrammarTools, not a generic OpenAI-compatible URL check. We only enables it only for known GPT-5+ OpenAI endpoints on known providers I tested. Any proxy stay off by default and must explicitly opt in:

{
  "providers": {
    "my-proxy": {
      "api": "openai-responses",
      "baseUrl": "https://proxy.example/v1",
      "compat": {
        "supportsGrammarTools": true
      },
      "models": [{ "id": "gpt-5.4" }]
    }
  }
}

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

Labels

to-discuss This ticket requires further discussion outside of triage.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Strict Tools / Grammar

2 participants