Add Tool Calling and Reasoning Token Metadata to genai_config.json with Fallback Map#2215
Add Tool Calling and Reasoning Token Metadata to genai_config.json with Fallback Map#2215sayanshaw24 wants to merge 9 commits into
genai_config.json with Fallback Map#2215Conversation
…into sayanshaw/tool-tags
There was a problem hiding this comment.
Pull request overview
This PR adds support for exposing model “generation tags” (tool-calling and reasoning start/end tokens) via genai_config.json, with a model-type-based fallback for older model artifacts. It extends the internal config/model layers and surfaces a generic tag getter through the public C API and C++ wrapper, plus adds C API tests to validate both fallback and explicit config parsing.
Changes:
- Extend config schema/parsing with optional
tool_callingandreasoningsections (Config::ToolCalling,Config::Reasoning). - Add
Model::GetTag(tag_name)with a static fallback map keyed bymodel.type. - Expose tag lookup via
OgaModelGetTag(C API) andOgaModel::GetTag(C++ wrapper), and add tests covering fallback and config-driven values.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/c_api_tests.cpp | Adds tests for tag fallback behavior and tag parsing from a temp genai_config.json. |
| src/ort_genai.h | Adds C++ wrapper OgaModel::GetTag(...) calling the new C API. |
| src/ort_genai_c.h | Adds public C API declaration/docs for OgaModelGetTag(...). |
| src/ort_genai_c.cpp | Implements OgaModelGetTag(...) by allocating a returned string from Model::GetTag. |
| src/models/model.h | Declares internal Model::GetTag(...) accessor (config + fallback). |
| src/models/model.cpp | Implements fallback map + config accessors for tag lookup. |
| src/config.h | Adds Config::ToolCalling and Config::Reasoning structs to the config model. |
| src/config.cpp | Wires new SAX elements (ToolCalling_Element, Reasoning_Element) into root parsing. |
9a62094 to
388b009
Compare
| return chunk_; | ||
| } | ||
|
|
||
| namespace { |
There was a problem hiding this comment.
Why do we need an anonymous namespace? Can we remove it?
| // This provides backward compatibility for Foundry Local when consuming older model | ||
| // packages that predate the bot/eot/bor/eor config fields. | ||
| if (bot_token_id_ < 0 || eot_token_id_ < 0 || bor_token_id_ < 0 || eor_token_id_ < 0) { | ||
| auto try_fallback = [&](int32_t& id, const std::string& tag_name) { |
There was a problem hiding this comment.
Can all this be merged inside GetFallbackTag so that try_fallback doesn't need to exist as a method?
| OGA_CATCH | ||
| } | ||
|
|
||
| OgaResult* OGA_API_CALL OgaTokenizerGetBotTokenId(const OgaTokenizer* tokenizer, int32_t* out) { |
There was a problem hiding this comment.
Can we add the APIs to the following language bindings as well?
- C#
- Java
- Objective-C
- Python
Add tool-calling and reasoning token IDs to GenAI Tokenizer
Summary
Adds four new token ID fields to the
modelsection ofgenai_config.json—bot_token_id,eot_token_id,bor_token_id,eor_token_id— and exposes them on the Tokenizer (alongsidebos,eos,pad), with a backward-compatible fallback for older model packages.This establishes a new naming convention for generation-level special tokens:
The naming mirrors
bos/eos/pad— short, unambiguous, and follows the same access pattern (GetBotTokenId()alongsideGetBosTokenId()).Changes
Config parsing (
config.h/config.cpp)Config::Model:bot_token_id,eot_token_id,bor_token_id,eor_token_id(int, default -1)Model_Element::OnValue()alongside existing token ID fields (bos, eos, pad)Tokenizer (
models/model.h/models/model.cpp)int32_t bot_token_id_,eot_token_id_,bor_token_id_,eor_token_id_GetBotTokenId(),GetEotTokenId(),GetBorTokenId(),GetEorTokenId()model.type:qwen2/qwen3/phi3→<tool_call>/</tool_call>gptoss→<|start|>/<|call|>qwen3→<think>/</think>(reasoning)Public C API (
ort_genai_c.h/ort_genai_c.cpp)OgaTokenizerGetBotTokenId(tokenizer, &token_id)— returns BOT token id or -1OgaTokenizerGetEotTokenId(tokenizer, &token_id)— returns EOT token id or -1OgaTokenizerGetBorTokenId(tokenizer, &token_id)— returns BOR token id or -1OgaTokenizerGetEorTokenId(tokenizer, &token_id)— returns EOR token id or -1Same pattern as
OgaTokenizerGetBosTokenId/OgaTokenizerGetPadTokenId.C++ wrapper (
ort_genai.h)int32_t OgaTokenizer::GetBotTokenId()int32_t OgaTokenizer::GetEotTokenId()int32_t OgaTokenizer::GetBorTokenId()int32_t OgaTokenizer::GetEorTokenId()Tests (
test/c_api_tests.cpp)TagId_Unknown— verifiesgpt2model (not in fallback map, no config IDs) returns -1 for all fourTagId_FromConfig— creates a temp model dir with token IDs in themodelsection, verifies correct parsing via the tokenizergenai_config.json format
{ "model": { "type": "qwen3", "bos_token_id": 151643, "eos_token_id": 151645, "bot_token_id": 151657, "eot_token_id": 151658, "bor_token_id": 151659, "eor_token_id": 151660, "decoder": { ... } } }All four fields are optional. If absent, the fallback map attempts to encode the known string via the tokenizer vocabulary. If the model type isn't in the fallback map either, -1 is returned (model doesn't support tool calling / reasoning).
Motivation
The Foundry Local C++ SDK currently detects tool-call and reasoning tokens by double-decoding every token through both a normal and a special-token tokenizer stream, then doing
string.find("tool_call")/string.find("think")per token. This is expensive.With token IDs exposed by GenAI on the Tokenizer, FL can do a single integer comparison per token in the decode loop — eliminating the special stream entirely for models with configured IDs.
Also required for the Catalog v2 migration where tool/reasoning metadata is being removed from catalog.
Design Decisions
Why on the Tokenizer (not Model)?
Tokenizer::GetBosTokenId()→Tokenizer::GetBotTokenId()Why bot/eot/bor/eor naming?
bos/eos/pad— short, memorable, consistenteot(end of text) since context is clearly "tool call" from the API nameboh/eohfor "harmony" channels, etc.)bot_token_idalongsidebos_token_idFallback map
Related PRs