Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7cdfc14
add initial tool and reasoning tag migration
Jun 11, 2026
5a053fe
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jun 23, 2026
476e71e
consolidate APIs
Jun 24, 2026
5d13270
Rename GetGenerationTag -> GetTag per review feedback from Scott
Jun 26, 2026
4da60c4
Clean up temp directory before test to avoid leftover collisions
Jun 29, 2026
e1df10c
Refactor: store tag token IDs in model section, expose GetTagId API
Jul 1, 2026
388b009
Merge origin/main into sayanshaw/tool-tags
Jul 1, 2026
f499725
Refactor: move tag IDs to Tokenizer, rename to bot/eot/bor/eor
Jul 8, 2026
43a695d
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jul 8, 2026
6276e87
Address review: simplify fallback, add C#/Java/ObjC/Python bindings
Jul 15, 2026
2260ede
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jul 15, 2026
1cb59fb
Fix clang-format in config.cpp switch/case block from main (unrelated)
Jul 15, 2026
e418c62
Fix phi3 fallback: use <|tool_call|>/<|/tool_call|> (with pipes)
Jul 15, 2026
03bce7b
Add reasoning fallback entries to qwen2 (covers DeepSeek think tags)
Jul 15, 2026
4270829
Use std::optional for tag IDs, extract fallback to tokenizer_tag_util…
Jul 22, 2026
4585871
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jul 22, 2026
c7f6ddf
Update comments: getters throw on unsupported, not return -1
Jul 23, 2026
8933b44
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jul 23, 2026
90feec9
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jul 24, 2026
b1a5aff
Standardize fallback map keys to bot/eot/bor/eor
Jul 24, 2026
b9b8b83
use fallback IDs, remove TokenToTokenId call, add Config::Defaults co…
Jul 24, 2026
26086e6
Merge branch 'main' of https://github.com/microsoft/onnxruntime-genai…
Jul 27, 2026
46677b9
Use Config::Defaults constants in fallback map keys for consistency
Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,14 @@ struct Model_Element : JSON::Element {
v_.left_context_samples = SafeDoubleToInt(JSON::Get<double>(value), name);
} else if (name == "right_context_samples") {
v_.right_context_samples = SafeDoubleToInt(JSON::Get<double>(value), name);
} else if (name == "bot_token_id") {
v_.bot_token_id = SafeDoubleToInt(JSON::Get<double>(value), name);
} else if (name == "eot_token_id") {
v_.eot_token_id = SafeDoubleToInt(JSON::Get<double>(value), name);
} else if (name == "bor_token_id") {
v_.bor_token_id = SafeDoubleToInt(JSON::Get<double>(value), name);
} else if (name == "eor_token_id") {
v_.eor_token_id = SafeDoubleToInt(JSON::Get<double>(value), name);
} else {
throw JSON::unknown_value_error{};
}
Expand Down
9 changes: 9 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ struct Config {
int video_token_id{};
int vision_start_token_id{};

// Tool-calling and reasoning token IDs.
// Follows the bos/eos/pad naming convention:
// bot = beginning of tool (call), eot = end of tool (call)
// bor = beginning of reasoning, eor = end of reasoning
std::optional<int> bot_token_id;
std::optional<int> eot_token_id;
std::optional<int> bor_token_id;
std::optional<int> eor_token_id;

int vocab_size{};
int context_length{};

Expand Down
16 changes: 16 additions & 0 deletions src/csharp/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,22 @@ public static extern UIntPtr OgaSequencesGetSequenceCount(IntPtr /* const OgaSeq
public static extern IntPtr /* OgaResult* */ OgaTokenizerGetPadTokenId(IntPtr /* const OgaTokenizer* */ tokenizer,
out int /* const int32_t* */ outPadTokenId);

[DllImport(NativeLib.DllName, CallingConvention = CallingConvention.Winapi)]
public static extern IntPtr /* OgaResult* */ OgaTokenizerGetBotTokenId(IntPtr /* const OgaTokenizer* */ tokenizer,
out int /* const int32_t* */ outBotTokenId);

[DllImport(NativeLib.DllName, CallingConvention = CallingConvention.Winapi)]
public static extern IntPtr /* OgaResult* */ OgaTokenizerGetEotTokenId(IntPtr /* const OgaTokenizer* */ tokenizer,
out int /* const int32_t* */ outEotTokenId);

[DllImport(NativeLib.DllName, CallingConvention = CallingConvention.Winapi)]
public static extern IntPtr /* OgaResult* */ OgaTokenizerGetBorTokenId(IntPtr /* const OgaTokenizer* */ tokenizer,
out int /* const int32_t* */ outBorTokenId);

[DllImport(NativeLib.DllName, CallingConvention = CallingConvention.Winapi)]
public static extern IntPtr /* OgaResult* */ OgaTokenizerGetEorTokenId(IntPtr /* const OgaTokenizer* */ tokenizer,
out int /* const int32_t* */ outEorTokenId);

[DllImport(NativeLib.DllName, CallingConvention = CallingConvention.Winapi)]
public static extern IntPtr /* OgaResult* */ OgaTokenizerEncode(IntPtr /* const OgaTokenizer* */ tokenizer,
byte[] /* const char* */ strings,
Expand Down
36 changes: 36 additions & 0 deletions src/csharp/Tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,42 @@ public int GetPadTokenId()
return padTokenId;
}

/// <summary>
/// Returns the BOT (beginning of tool call) token ID, or -1 if not defined.
/// </summary>
public int GetBotTokenId()
{
Result.VerifySuccess(NativeMethods.OgaTokenizerGetBotTokenId(_tokenizerHandle, out int botTokenId));
return botTokenId;
}

/// <summary>
/// Returns the EOT (end of tool call) token ID, or -1 if not defined.
/// </summary>
public int GetEotTokenId()
{
Result.VerifySuccess(NativeMethods.OgaTokenizerGetEotTokenId(_tokenizerHandle, out int eotTokenId));
return eotTokenId;
}

/// <summary>
/// Returns the BOR (beginning of reasoning) token ID, or -1 if not defined.
/// </summary>
public int GetBorTokenId()
{
Result.VerifySuccess(NativeMethods.OgaTokenizerGetBorTokenId(_tokenizerHandle, out int borTokenId));
return borTokenId;
}

/// <summary>
/// Returns the EOR (end of reasoning) token ID, or -1 if not defined.
/// </summary>
public int GetEorTokenId()
{
Result.VerifySuccess(NativeMethods.OgaTokenizerGetEorTokenId(_tokenizerHandle, out int eorTokenId));
return eorTokenId;
}

public TokenizerStream CreateStream()
{
IntPtr tokenizerStreamHandle = IntPtr.Zero;
Expand Down
64 changes: 64 additions & 0 deletions src/java/src/main/java/ai/onnxruntime/genai/Tokenizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,62 @@ public int getPadTokenId() throws GenAIException {
return tokenizerGetPadTokenId(nativeHandle);
}

/**
* Gets the BOT (beginning of tool call) token ID, or -1 if the model does not define one.
*
* @return The BOT token ID.
* @throws GenAIException If the call to the GenAI native API fails.
*/
public int getBotTokenId() throws GenAIException {
if (nativeHandle == 0) {
throw new IllegalStateException("Instance has been freed and is invalid");
}

return tokenizerGetBotTokenId(nativeHandle);
}

/**
* Gets the EOT (end of tool call) token ID, or -1 if the model does not define one.
*
* @return The EOT token ID.
* @throws GenAIException If the call to the GenAI native API fails.
*/
public int getEotTokenId() throws GenAIException {
if (nativeHandle == 0) {
throw new IllegalStateException("Instance has been freed and is invalid");
}

return tokenizerGetEotTokenId(nativeHandle);
}

/**
* Gets the BOR (beginning of reasoning) token ID, or -1 if the model does not define one.
*
* @return The BOR token ID.
* @throws GenAIException If the call to the GenAI native API fails.
*/
public int getBorTokenId() throws GenAIException {
if (nativeHandle == 0) {
throw new IllegalStateException("Instance has been freed and is invalid");
}

return tokenizerGetBorTokenId(nativeHandle);
}

/**
* Gets the EOR (end of reasoning) token ID, or -1 if the model does not define one.
*
* @return The EOR token ID.
* @throws GenAIException If the call to the GenAI native API fails.
*/
public int getEorTokenId() throws GenAIException {
if (nativeHandle == 0) {
throw new IllegalStateException("Instance has been freed and is invalid");
}

return tokenizerGetEorTokenId(nativeHandle);
}

/**
* Gets the end of sentence token IDs.
*
Expand Down Expand Up @@ -229,6 +285,14 @@ public void close() {

private native int[] tokenizerGetEosTokenIds(long tokenizerHandle) throws GenAIException;

private native int tokenizerGetBotTokenId(long tokenizerHandle) throws GenAIException;

private native int tokenizerGetEotTokenId(long tokenizerHandle) throws GenAIException;

private native int tokenizerGetBorTokenId(long tokenizerHandle) throws GenAIException;

private native int tokenizerGetEorTokenId(long tokenizerHandle) throws GenAIException;

private native int tokenizerToTokenId(long tokenizerHandle, String str) throws GenAIException;

private native String tokenizerApplyChatTemplate(
Expand Down
48 changes: 48 additions & 0 deletions src/java/src/main/native/ai_onnxruntime_genai_Tokenizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,54 @@ Java_ai_onnxruntime_genai_Tokenizer_tokenizerGetPadTokenId(JNIEnv* env, jobject
return static_cast<jint>(token_id);
}

JNIEXPORT jint JNICALL
Java_ai_onnxruntime_genai_Tokenizer_tokenizerGetBotTokenId(JNIEnv* env, jobject thiz, jlong tokenizer_handle) {
const OgaTokenizer* tokenizer = reinterpret_cast<const OgaTokenizer*>(tokenizer_handle);
int32_t token_id = 0;

if (ThrowIfError(env, OgaTokenizerGetBotTokenId(tokenizer, &token_id))) {
return 0;
}

return static_cast<jint>(token_id);
}

JNIEXPORT jint JNICALL
Java_ai_onnxruntime_genai_Tokenizer_tokenizerGetEotTokenId(JNIEnv* env, jobject thiz, jlong tokenizer_handle) {
const OgaTokenizer* tokenizer = reinterpret_cast<const OgaTokenizer*>(tokenizer_handle);
int32_t token_id = 0;

if (ThrowIfError(env, OgaTokenizerGetEotTokenId(tokenizer, &token_id))) {
return 0;
}

return static_cast<jint>(token_id);
}

JNIEXPORT jint JNICALL
Java_ai_onnxruntime_genai_Tokenizer_tokenizerGetBorTokenId(JNIEnv* env, jobject thiz, jlong tokenizer_handle) {
const OgaTokenizer* tokenizer = reinterpret_cast<const OgaTokenizer*>(tokenizer_handle);
int32_t token_id = 0;

if (ThrowIfError(env, OgaTokenizerGetBorTokenId(tokenizer, &token_id))) {
return 0;
}

return static_cast<jint>(token_id);
}

JNIEXPORT jint JNICALL
Java_ai_onnxruntime_genai_Tokenizer_tokenizerGetEorTokenId(JNIEnv* env, jobject thiz, jlong tokenizer_handle) {
const OgaTokenizer* tokenizer = reinterpret_cast<const OgaTokenizer*>(tokenizer_handle);
int32_t token_id = 0;

if (ThrowIfError(env, OgaTokenizerGetEorTokenId(tokenizer, &token_id))) {
return 0;
}

return static_cast<jint>(token_id);
}

JNIEXPORT jintArray JNICALL
Java_ai_onnxruntime_genai_Tokenizer_tokenizerGetEosTokenIds(JNIEnv* env, jobject thiz, jlong tokenizer_handle) {
const OgaTokenizer* tokenizer = reinterpret_cast<const OgaTokenizer*>(tokenizer_handle);
Expand Down
35 changes: 34 additions & 1 deletion src/models/model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@
#include <algorithm>
#include <array>
#include <climits>
#include <functional>
#include <random>
#include <set>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>

#include "../generators.h"
#include "../search.h"
#include "../tracing.h"
#include "model.h"
#include "model_package.h"
#include "tokenizer_tag_utils.h"
#include "gpt.h"
#include "decoder_only.h"
#include "whisper.h"
Expand Down Expand Up @@ -308,14 +311,44 @@ const std::string& TokenizerStream::Decode(int32_t token) {

Tokenizer::Tokenizer(Config& config) : bos_token_id_{config.model.bos_token_id},
eos_token_id_{config.model.eos_token_id},
pad_token_id_{config.model.pad_token_id} {
pad_token_id_{config.model.pad_token_id},
bot_token_id_{config.model.bot_token_id},
eot_token_id_{config.model.eot_token_id},
bor_token_id_{config.model.bor_token_id},
eor_token_id_{config.model.eor_token_id} {
// Default tokenizer options
const char* keys[] = {"add_special_tokens", "skip_special_tokens"};
const char* values[] = {"false", "true"};

// Resolve tokenizer_dir (may be empty, relative, absolute, or a "sha256:" shared-asset reference).
const fs::path tokenizer_dir = config.ResolvePath(config.model.tokenizer_dir);
CheckResult(OrtxCreateTokenizerWithOptions(tokenizer_.Address(), tokenizer_dir.string().c_str(), keys, values, 2));

// Resolve any unset bot/eot/bor/eor IDs via model-type fallback strings.
if (!bot_token_id_) bot_token_id_ = ResolveFallbackTokenId(config.model.type, "tool_call_start", *this);
Comment thread
baijumeswani marked this conversation as resolved.
Outdated
if (!eot_token_id_) eot_token_id_ = ResolveFallbackTokenId(config.model.type, "tool_call_end", *this);
if (!bor_token_id_) bor_token_id_ = ResolveFallbackTokenId(config.model.type, "reasoning_start", *this);
if (!eor_token_id_) eor_token_id_ = ResolveFallbackTokenId(config.model.type, "reasoning_end", *this);
}

int32_t Tokenizer::GetBotTokenId() const {
if (!bot_token_id_) throw std::runtime_error("bot_token_id is not defined for this model");
return *bot_token_id_;
}

int32_t Tokenizer::GetEotTokenId() const {
if (!eot_token_id_) throw std::runtime_error("eot_token_id is not defined for this model");
return *eot_token_id_;
}

int32_t Tokenizer::GetBorTokenId() const {
if (!bor_token_id_) throw std::runtime_error("bor_token_id is not defined for this model");
return *bor_token_id_;
}

int32_t Tokenizer::GetEorTokenId() const {
if (!eor_token_id_) throw std::runtime_error("eor_token_id is not defined for this model");
return *eor_token_id_;
}

std::unique_ptr<TokenizerStream> Tokenizer::CreateStream() const {
Expand Down
15 changes: 15 additions & 0 deletions src/models/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "ortx_tokenizer.h"
#include "../generators.h"
#include "utils.h"
#include <optional>
#include "phi_image_processor.h"
#include "whisper_processor.h"
#include "parakeet_processor.h"
Expand Down Expand Up @@ -106,12 +107,26 @@ struct Tokenizer : std::enable_shared_from_this<Tokenizer>, LeakChecked<Tokenize
const std::vector<int32_t>& GetEosTokenIds() const { return eos_token_id_; }
int32_t GetPadTokenId() const { return pad_token_id_; }

// Tool-calling and reasoning token IDs.
// Naming follows the bos/eos/pad convention:
// bot = beginning of tool (call), eot = end of tool (call)
// bor = beginning of reasoning, eor = end of reasoning
// Throws if the model does not define the requested token.
int32_t GetBotTokenId() const;
int32_t GetEotTokenId() const;
int32_t GetBorTokenId() const;
int32_t GetEorTokenId() const;

OrtxPtr<OrtxTokenizer> tokenizer_;

private:
int32_t bos_token_id_;
std::vector<int32_t> eos_token_id_;
int32_t pad_token_id_;
std::optional<int32_t> bot_token_id_;
std::optional<int32_t> eot_token_id_;
std::optional<int32_t> bor_token_id_;
std::optional<int32_t> eor_token_id_;
};

struct MultiModalProcessor : std::enable_shared_from_this<MultiModalProcessor>, ExternalRefCounted<MultiModalProcessor> {
Expand Down
29 changes: 29 additions & 0 deletions src/models/tokenizer_tag_utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include "tokenizer_tag_utils.h"
#include "model.h"

namespace Generators {

std::optional<int32_t> ResolveFallbackTokenId(const std::string& model_type,
const std::string& tag_name,
const Tokenizer& tokenizer) {
static const std::unordered_map<std::string, std::unordered_map<std::string, std::string>> fallback_map = {
{"qwen2", {{"tool_call_start", "<tool_call>"}, {"tool_call_end", "</tool_call>"}, {"reasoning_start", "<think>"}, {"reasoning_end", "</think>"}}},
{"qwen3", {{"tool_call_start", "<tool_call>"}, {"tool_call_end", "</tool_call>"}, {"reasoning_start", "<think>"}, {"reasoning_end", "</think>"}}},
{"phi3", {{"tool_call_start", "<|tool_call|>"}, {"tool_call_end", "<|/tool_call|>"}}},
{"gptoss", {{"tool_call_start", "<|start|>"}, {"tool_call_end", "<|call|>"}}},
};

auto type_it = fallback_map.find(model_type);
if (type_it == fallback_map.end()) return std::nullopt;
auto tag_it = type_it->second.find(tag_name);
if (tag_it == type_it->second.end()) return std::nullopt;

int32_t resolved = tokenizer.TokenToTokenId(tag_it->second.c_str());
Comment thread
baijumeswani marked this conversation as resolved.
Outdated
if (resolved >= 0) return resolved;
return std::nullopt;
}

} // namespace Generators
25 changes: 25 additions & 0 deletions src/models/tokenizer_tag_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once

#include <optional>
#include <string>
#include <unordered_map>

namespace Generators {

struct Tokenizer;

// Resolves a fallback token ID for models whose genai_config.json doesn't yet include
// bot/eot/bor/eor token IDs in the model section. This exists specifically for
// Foundry Local backward compatibility with older model packages that predate
// these config fields.
//
// Returns the resolved token ID if found in the fallback map and tokenizer vocabulary,
// or std::nullopt if the model type/tag name is not in the map or the token string
// doesn't resolve in the vocabulary.
std::optional<int32_t> ResolveFallbackTokenId(const std::string& model_type,
const std::string& tag_name,
const Tokenizer& tokenizer);

} // namespace Generators
Loading
Loading