Skip to content

feat: Kimi K3 structural tag default - #196

Merged
indrajit96 merged 14 commits into
mainfrom
ibhosale/k3-kvv-crates
Sep 29, 2026
Merged

indrajit96 merged 14 commits into
mainfrom
ibhosale/k3-kvv-crates

Conversation

@indrajit96

@indrajit96 indrajit96 commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is the grammar-building half of a coordinated change addressing two
tool-calling correctness problems:

  1. Kimi K3 grammar correctness. K3 tool_choice="auto" requests could begin
    a native XTML tool call but still emit undeclared tools, empty required
    arguments, incorrectly typed values, duplicate required arguments, or
    malformed XTML that the parser could not consume.

  2. Schema enforcement for default-on structural guidance. The generic
    structural-tag policy treated an omitted OpenAI strict field as relaxed.
    The new policy uses the declared schema when strict is omitted or true;
    only explicit strict:false opts out.

This PR implements those parser and grammar changes. The companion
ai-dynamo/dynamo#13654
owns deployment defaults, activation policy, frontend integration, and
compatibility fallback.

Relationship to #274 and #277

  • #274 addresses
    nullable Kimi K3 arguments. feat: Kimi K3 structural tag default #196 already emits a declared-key XTML
    alternative for each representable type in anyOf and type: [...]
    unions. Its grammar retains the string/null constraints and excludes
    numeric values and unknown argument keys in those cases.
  • #277 addresses
    local $ref arguments. feat: Kimi K3 structural tag default #196 already resolves references against the
    complete tool schema and retains typed object and enum constraints,
    including recursive definitions. We adapted fix(parsers): resolve Kimi K3 argument schema references #277's remaining pointer
    decoding, array-index lookup, expansion limits, and reference-scope guards
    into this PR in merge commit d53f4ca. When inlining is unsafe, the
    original explicitly typed schema is retained.

Kimi K3 grammar hardening

Kimi K3 emits tool calls through its native XTML tools, call, and argument
elements. The new auto builder follows that model-native format while
preserving the semantics of automatic tool choice:

  • ordinary assistant text remains unconstrained;
  • guidance activates lazily only after the model emits
    <|open|>tools<|sep|>;
  • calls are restricted to tool names declared in the request;
  • automatic calls require positive call indices;
  • parallel_tool_calls:false limits generation to one call;
  • required string arguments must be non-empty;
  • integer, number, boolean, array, object, and null arguments use typed content;
  • $ref, $defs, anyOf, oneOf, allOf, enum, and const participate in
    XTML type inference;
  • required arguments use the schema's canonical required order, avoiding
    factorial grammar expansion;
  • loose text between arguments is replaced by complete declared optional
    argument tags, preventing duplicate required keys from overwriting a
    constrained value;
  • schemas with only optional properties may emit an empty argument body;
  • ambiguous schemas fall back to parser-compatible XTML instead of generating
    malformed calls; and
  • named tool choice prevents a second generic tool-call representation from
    being emitted in the response channel.

TriggeredTagsFormat also gains optional excludes, allowing the K3 grammar to
reject invalid reasoning and call-marker transitions before the tools trigger.
Existing builders serialize unchanged when no exclusions are configured.

flowchart LR
    A[K3 begins generation] --> B{Opens tools channel?}
    B -- No --> C[Normal assistant answer]
    B -- Yes --> D[Triggered K3 grammar]
    D --> E[Select a declared tool]
    E --> F[Generate call index]
    F --> G{Schema representable?}
    G -- Yes --> H[Required typed arguments followed by declared optional arguments]
    G -- No --> I[Parser-compatible XTML fallback]
    H --> J[Valid native K3 call]
    I --> J
    J --> K[K3 parser produces OpenAI tool_calls]
Loading

tool_choice="auto" still does not force a tool call. The grammar constrains a
call only after the model chooses to begin the native tools channel.

Shared schema behavior

StructuralTagSchemaMode::Auto now follows these rules for all structural-tag
builders:

Tool setting Argument guidance
strict omitted Use the declared parameter schema
strict:true Use the declared parameter schema
strict:false Keep the native envelope but relax argument-schema enforcement
Global schema mode strict Use the declared schema, overriding strict:false

This PR does not change parser-to-model mappings or deployment defaults.

Correctness validation

Kimi K3 Dynamo and SGLang KVV correctness before and after structural guidance

With Dynamo and SGLang, K3 KVV correctness improved:

  • non-thinking: 25/49 = 51.0% to 48/49 = 98.0%;
  • thinking: 69/98 = 70.4% to 94/98 = 95.9%.

The controlled native SGLang ablation provides the strongest causal evidence:

  • structural guidance enabled: 47/48 = 97.9% non-thinking and
    90/92 = 97.8% thinking;
  • structural guidance disabled: 22/48 = 45.8% and 61/92 = 66.3%;
  • re-enabling guidance restored the original scores.

An activation-only Dynamo patch improved non-thinking to 37/49 but did not
repair thinking correctness, exposing the old empty and weakly typed argument
grammar. The complete builder was needed for the final result.

A later near-full Dynamo and vLLM run using this frontend-crates head reached
47/48 = 97.9% non-thinking and 83/97 = 85.6% thinking after excluding
context-length cases.

Performance validation

Kimi K3 p50 TTFT with and without the structural-guidance changes

The tested Kimi workloads did not show a TTFT regression:

  • concurrency-1 probe: 0.400 s without the change and 0.356 s with it;
  • concurrency-24 streaming: 1.92 s without the change and 1.79 s with it.

These are two measured workload shapes and should not be interpreted as a
general performance improvement.

Unit validation

Focused tests cover:

  • optional auto activation and normal-answer passthrough;
  • thinking-to-tools transitions;
  • declared and named tool selection;
  • required and parallel calls;
  • canonical required-argument ordering;
  • duplicate-required-key prevention;
  • optional-only schemas and empty argument bodies;
  • typed string, numeric, boolean, array, object, and null arguments;
  • local references and schema-composition keywords;
  • ambiguous-schema fallback; and
  • serialization compatibility for existing structural-tag builders.

Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
@github-actions github-actions Bot added the feat label Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

📊 Conformance matrix rendered — view in CI summary

@rmccorm4 rmccorm4 changed the title feat: K3 structual tag defualt feat: K3 structural tag default Aug 22, 2026
@rmccorm4 rmccorm4 changed the title feat: K3 structural tag default feat: Kimi K3 structural tag default Aug 23, 2026
@indrajit96
indrajit96 marked this pull request as ready for review August 24, 2026 20:52
@indrajit96
indrajit96 requested a review from a team as a code owner August 24, 2026 20:52
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>

@rmccorm4 rmccorm4 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.

Local exact-head review findings.

Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
ccrvlh pushed a commit to aircloud-dev/frontend-crates that referenced this pull request Sep 16, 2026
VENDORED, NOT OUR WORK. This is upstream pull request ai-dynamo#196
("feat: Kimi K3 structural tag default", branch `ibhosale/k3-kvv-crates`),
applied here byte for byte and authored by Indrajit Bhosale. It is carried
only because that pull request has not merged yet, and it must be dropped the
moment it does. The original subject line was `K3 structual tag defualt`; only
this message has been rewritten, never the diff.

What it does: the Kimi K3 structural-tag builder previously had a real grammar
only for the mandatory tool-choice paths, so `tool_choice: auto` fell back to a
loose tag. This adds `build_auto_structural_tag`, which derives the tag from
each tool's declared JSON schema:

  - `JsonType` maps the JSON schema types onto their xtml names, resolving
    local `$ref` targets against the root `$defs` so referenced argument
    schemas keep their definitions in typed content.
  - Required arguments are emitted in canonical (schema-declaration) order,
    each with its own typed tag; optional arguments follow in a repeatable
    slot.
  - The thinking block is closed before the triggered-tools suffix, so a tool
    call cannot open inside an unterminated reasoning span.
  - `strict: false` still selects permissive argument bodies.

`TriggeredTagsFormat` also gains an `excludes` list, serialized only when
non-empty, threaded with an empty default through the DSML, Kimi K2 and
generic triggered-tags constructors.

Note for the reader of this branch: as vendored, this commit also reverts the
split between Dynamo's generic argument-schema policy and Kimi's, because the
pull request branch predates that split. The commit that follows restores it.

Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
ccrvlh added a commit to aircloud-dev/frontend-crates that referenced this pull request Sep 16, 2026
Review fix against the preceding commit (upstream pull request ai-dynamo#196), whose
branch predates the split between Dynamo's generic argument-schema policy and
Kimi's. Applying it routes every generic builder back through
`uses_declared_tool_schema`, where an omitted `strict` field selects the
tool's declared parameter schema.

That is the wrong default off Kimi. In OpenAI's structured-outputs semantics
`strict` is an opt-in: absent means not strict. The generic builders serve
hermes, qwen25, qwen3_coder, inkling and deepseek_dsml, whose clients
overwhelmingly omit the field, so constraining their arguments to a declared
schema — one that is frequently incomplete — changes output that previously
decoded freely, without anyone asking for it.

Restore the split. `resolve_tool_schema` keeps `strict: true`-only semantics
for the generic builders. The Kimi K2 and K3 builders use
`kimi_uses_declared_tool_schema`, which matches vLLM/xgrammar in treating only
an explicit `strict: false` as an opt-out, and global strict mode still
overrides the per-tool opt-out on both paths. The two policies are pinned by
separate tests so neither can drift into the other.

Signed-off-by: Caio Rocha <caio@crafthouse.io>
ccrvlh added a commit to aircloud-dev/frontend-crates that referenced this pull request Sep 16, 2026
Review fix against upstream pull request ai-dynamo#196, vendored two commits back.

`optional_auto_arguments` built a tag only for properties that
`auto_argument_format` could reduce to a single xtml type; every other
property was dropped by the `filter_map`. Dropped means the grammar has no
path to that argument at all, so the model can never emit it, however the
caller declared it.

The shapes that hit this are ordinary, not exotic:

  - `anyOf: [{"type": "string"}, {"type": "null"}]`, which is what Pydantic
    emits for `Optional[X]`,
  - an explicit multi-type list, `{"type": ["integer", "null"]}`,
  - a description-only property carrying no `type` at all.

When a tool's optional properties are all of that kind and it declares no
required ones, the arguments block collapses to a const empty string and the
tool becomes callable only with no arguments.

Give each such property a tag of its own instead, keyed to its name, with the
`type` attribute restricted by regex to the xtml type names its schema
actually admits and only the body left free. Keying the tag is the point: a
bare permissive tag in the optional slot could repeat a required key and
overwrite its constrained value, which is the duplicate-emission failure this
path must not reopen. A `false` schema admits no value and is still dropped.

Every existing fixture used exclusively single-typed properties, which is why
the drop went unnoticed; the four tests added here pin the shapes real tool
declarations contain.

Signed-off-by: Caio Rocha <caio@crafthouse.io>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
rmccorm4 and others added 3 commits September 29, 2026 12:17
Signed-off-by: Ryan McCormick <rmccormick@nvidia.com>
Signed-off-by: Indrajit Bhosale <iamindrajitb@gmail.com>
Signed-off-by: Ryan McCormick <rmccormick@nvidia.com>
@rmccorm4

Copy link
Copy Markdown
Contributor

/ok to test 75e882f

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants