Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
36 changes: 29 additions & 7 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 Expand Up @@ -1416,13 +1424,27 @@ static std::string EscapeJsonString(std::string_view s) {
result.reserve(s.size());
for (char c : s) {
switch (c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\b': result += "\\b"; break;
case '\f': result += "\\f"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
case '"':
result += "\\\"";
break;
case '\\':
result += "\\\\";
break;
case '\b':
result += "\\b";
break;
case '\f':
result += "\\f";
break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
if (static_cast<unsigned char>(c) < 0x20) {
throw std::runtime_error(
Expand Down
10 changes: 10 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ 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
// -1 means the model does not define this token.
int bot_token_id{-1};
int eot_token_id{-1};
int bor_token_id{-1};
int eor_token_id{-1};
Comment thread
sayanshaw24 marked this conversation as resolved.
Outdated

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
41 changes: 40 additions & 1 deletion src/models/model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
#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"
Expand Down Expand Up @@ -306,16 +308,53 @@ const std::string& TokenizerStream::Decode(int32_t token) {
return chunk_;
}

// Fallback: if the given token ID is unset (-1), attempt to resolve it by looking up
// a well-known token string for the model type in the tokenizer vocabulary.
// This provides backward compatibility for Foundry Local when consuming older model
// packages that predate the bot/eot/bor/eor config fields.
// Keyed by model.type string from genai_config.json.
static void ResolveFallbackTagId(int32_t& id, const std::string& model_type,
Comment thread
sayanshaw24 marked this conversation as resolved.
Outdated
Comment thread
sayanshaw24 marked this conversation as resolved.
Outdated
const std::string& tag_name, const Tokenizer& tokenizer) {
if (id >= 0) return;

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;
auto tag_it = type_it->second.find(tag_name);
if (tag_it == type_it->second.end()) return;

int32_t resolved = tokenizer.TokenToTokenId(tag_it->second.c_str());
if (resolved >= 0) id = resolved;
}

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_ < 0 || eot_token_id_ < 0 || bor_token_id_ < 0 || eor_token_id_ < 0) {
ResolveFallbackTagId(bot_token_id_, config.model.type, "tool_call_start", *this);
ResolveFallbackTagId(eot_token_id_, config.model.type, "tool_call_end", *this);
ResolveFallbackTagId(bor_token_id_, config.model.type, "reasoning_start", *this);
ResolveFallbackTagId(eor_token_id_, config.model.type, "reasoning_end", *this);
}
}

std::unique_ptr<TokenizerStream> Tokenizer::CreateStream() const {
Expand Down
14 changes: 14 additions & 0 deletions src/models/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,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
// Returns -1 if the model does not define the token.
int32_t GetBotTokenId() const { return bot_token_id_; }
int32_t GetEotTokenId() const { return eot_token_id_; }
int32_t GetBorTokenId() const { return bor_token_id_; }
int32_t GetEorTokenId() const { return eor_token_id_; }

OrtxPtr<OrtxTokenizer> tokenizer_;

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

struct MultiModalProcessor : std::enable_shared_from_this<MultiModalProcessor>, ExternalRefCounted<MultiModalProcessor> {
Expand Down
20 changes: 20 additions & 0 deletions src/objectivec/include/ort_genai_objc.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ typedef NS_ENUM(NSInteger, OGAElementType) {
*/
- (int32_t)getPadTokenId:(NSError**)error;

/**
* Return the BOT (beginning of tool call) token ID, or -1 if not defined.
*/
- (int32_t)getBotTokenId:(NSError**)error;

/**
* Return the EOT (end of tool call) token ID, or -1 if not defined.
*/
- (int32_t)getEotTokenId:(NSError**)error;

/**
* Return the BOR (beginning of reasoning) token ID, or -1 if not defined.
*/
- (int32_t)getBorTokenId:(NSError**)error;

/**
* Return the EOR (end of reasoning) token ID, or -1 if not defined.
*/
- (int32_t)getEorTokenId:(NSError**)error;

/**
* Encode text to sequences
*
Expand Down
Loading
Loading