Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
34 changes: 10 additions & 24 deletions include/ortx_tokenizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ extError_t ORTX_API_CALL OrtxCreateTokenizer(OrtxTokenizer** tokenizer, const ch
* - Values: `"true"` / `"false"` or `"1"` / `"0"`;
* - Default: `"true"`
*
* - `chat_template_kwargs`
* - Purpose: Adds typed values to the chat template context;
* - Values: A serialized JSON object, such as `{"enable_thinking":false}`;
* - Default: `{}`. Set the value to `{}` to clear previously configured values.
*
Comment thread
jennyf19 marked this conversation as resolved.
* Future tokenizer options may be added without changing this API signature.
*
* \see OrtxUpdateTokenizerOptions for updating options on an existing tokenizer.
Expand Down Expand Up @@ -127,6 +132,11 @@ extError_t ORTX_API_CALL OrtxCreateTokenizerFromBlob(OrtxTokenizer** tokenizer,
* - Values: `"true"` / `"false"` or `"1"` / `"0"`;
* - Default: `"true"`
*
* - `chat_template_kwargs`
* - Purpose: Adds typed values to the chat template context;
* - Values: A serialized JSON object, such as `{"enable_thinking":false}`;
* - Default: `{}`. Set the value to `{}` to clear previously configured values.
*
* Future tokenizer options may be added without changing this API signature.
*
*/
Expand Down Expand Up @@ -276,30 +286,6 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplate(const OrtxTokenizer* tokenizer, c
const char* input, const char* tools, OrtxTensorResult** output,
bool add_generation_prompt, bool tokenize);

/**
* @brief Applies a chat template with additional template context values.
*
* Behaves like OrtxApplyChatTemplate, while also adding the properties from
* template_kwargs to the chat template context. template_kwargs must be a
* null-terminated JSON object or null. Core context properties such as messages,
* tools, and add_generation_prompt cannot be overridden.
*
* @param tokenizer Pointer to an OrtxTokenizer used for template processing.
* @param template_str Null-terminated string representing the chat template; can be null if tokenizer.json has one.
* @param input Null-terminated string containing the input to be processed.
* @param tools Null-terminated string containing the function tools.
* @param template_kwargs Null-terminated JSON object containing additional template context values; can be null.
* @param output Pointer to an OrtxTensorResult that will be populated with the output strings,
* if tokenize is true, the ids will be in the output as indexed 1.
* @param add_generation_prompt Indicates whether to add a generation prompt to the output.
* @param tokenize Indicates whether to tokenize the templated text to IDs.
* @return extError_t Returns an error code indicating success or the type of failure.
*/
extError_t ORTX_API_CALL OrtxApplyChatTemplateWithOptions(const OrtxTokenizer* tokenizer, const char* template_str,
const char* input, const char* tools,
const char* template_kwargs, OrtxTensorResult** output,
bool add_generation_prompt, bool tokenize);

#ifdef __cplusplus
}
#endif
4 changes: 2 additions & 2 deletions pyop/py_c_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ void AddGlobalMethodsCApi(pybind11::module& m) {

return reinterpret_cast<std::uintptr_t>(tokenizer);
},
"Create a tokenizer with options (like add_special_tokens, skip_special_tokens).");
"Create a tokenizer with options (like add_special_tokens, skip_special_tokens, or chat_template_kwargs).");

m.def(
"update_tokenizer_options",
Expand All @@ -187,7 +187,7 @@ void AddGlobalMethodsCApi(pybind11::module& m) {
throw std::runtime_error(std::string("Failed to update tokenizer options\n") + OrtxGetLastErrorMessage());
}
},
"Update existing tokenizer options, e.g., add_special_tokens or skip_special_tokens.");
"Update existing tokenizer options, e.g., add_special_tokens, skip_special_tokens, or chat_template_kwargs.");

m.def(
"batch_tokenize",
Expand Down
27 changes: 17 additions & 10 deletions shared/api/c_api_tokenizer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <regex>
#include <algorithm>

#include "nlohmann/json.hpp"

#include "c_api_utils.hpp"
#include "tokenizer_impl.h"

Expand Down Expand Up @@ -52,7 +54,8 @@ static std::unordered_map<std::string, std::string> BuildOptionsMap(const char*
// Define the set of valid option keys - may be added to in the future
static const std::unordered_set<std::string> valid_keys = {
"add_special_tokens",
"skip_special_tokens"
"skip_special_tokens",
"chat_template_kwargs"
};

std::unordered_map<std::string, std::string> options;
Expand All @@ -68,6 +71,18 @@ static std::unordered_map<std::string, std::string> BuildOptionsMap(const char*
return {};
}

if (key == "chat_template_kwargs") {
auto parsed_kwargs = nlohmann::json::parse(values[i], nullptr, /*allow_exceptions=*/false);
if (parsed_kwargs.is_discarded()) {
ReturnableStatus::last_error_message_ = "Invalid chat_template_kwargs JSON.";
return {};
Comment thread
jennyf19 marked this conversation as resolved.
Outdated
}
if (!parsed_kwargs.is_object()) {
ReturnableStatus::last_error_message_ = "chat_template_kwargs must be a JSON object.";
return {};
}
}

options[key] = values[i];
}
}
Expand Down Expand Up @@ -467,14 +482,6 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplate(const OrtxTokenizer* tokenizer, c
const char* input, const char* tools,
OrtxTensorResult** output, bool add_generation_prompt,
bool tokenize) {
return OrtxApplyChatTemplateWithOptions(tokenizer, template_str, input, tools, nullptr, output,
add_generation_prompt, tokenize);
}

extError_t ORTX_API_CALL OrtxApplyChatTemplateWithOptions(const OrtxTokenizer* tokenizer, const char* template_str,
const char* input, const char* tools,
const char* template_kwargs, OrtxTensorResult** output,
bool add_generation_prompt, bool tokenize) {
if (tokenizer == nullptr) {
ReturnableStatus::last_error_message_ = "tokenizer is null";
return kOrtxErrorInvalidArgument;
Expand All @@ -493,7 +500,7 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplateWithOptions(const OrtxTokenizer* t

std::string text;
std::vector<extTokenId_t> ids_vec;
status = token_ptr->ApplyChatTemplate(template_str, input, tools, template_kwargs, text, ids_vec,
status = token_ptr->ApplyChatTemplate(template_str, input, tools, text, ids_vec,
add_generation_prompt, tokenize);
if (status.IsOk()) {
auto result = std::make_unique<ort_extensions::TensorResult>();
Expand Down
17 changes: 7 additions & 10 deletions shared/api/chat_template.cc
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,8 @@ std::string normalize_tool_quotes(const std::string& input) {
}

OrtxStatus TokenizerImpl::ApplyChatTemplate(const char* template_str, const char* message, const char* tools,
const char* template_kwargs, std::string& output,
std::vector<extTokenId_t>& ids_vec, bool add_generation_prompt,
bool tokenize) const {
std::string& output, std::vector<extTokenId_t>& ids_vec,
bool add_generation_prompt, bool tokenize) const {
OrtxStatus status;
std::string input_str = minja::normalize_newlines(message);

Expand Down Expand Up @@ -410,17 +409,15 @@ OrtxStatus TokenizerImpl::ApplyChatTemplate(const char* template_str, const char
}

json context_values = json::object();
if (template_kwargs) {
if (*template_kwargs == '\0') {
throw std::runtime_error("template_kwargs must be a JSON object or null.");
}
auto parsed_kwargs = json::parse(minja::normalize_newlines(template_kwargs), nullptr,
const auto template_kwargs = options_map.find("chat_template_kwargs");
if (template_kwargs != options_map.end()) {
auto parsed_kwargs = json::parse(minja::normalize_newlines(template_kwargs->second), nullptr,
/*allow_exceptions=*/false);
Comment thread
jennyf19 marked this conversation as resolved.
if (parsed_kwargs.is_discarded()) {
throw std::runtime_error("Invalid template_kwargs JSON.");
throw std::runtime_error("Invalid chat_template_kwargs JSON.");
}
if (!parsed_kwargs.is_object()) {
throw std::runtime_error("template_kwargs must be a JSON object.");
throw std::runtime_error("chat_template_kwargs must be a JSON object.");
}
context_values = std::move(parsed_kwargs);
}
Expand Down
4 changes: 2 additions & 2 deletions shared/api/tokenizer_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ class TokenizerImpl : public OrtxObjectImpl {
OrtxStatus GetDecoderPromptIds(size_t batch_size, const char* lang, const char* task, int no_timestamps,
std::vector<std::vector<extTokenId_t>>& t_ids) const;
OrtxStatus ApplyChatTemplate(const char* template_str, const char* message, const char* tools,
const char* template_kwargs, std::string& output,
std::vector<extTokenId_t>& ids_vec, bool add_generation_prompt, bool tokenize) const;
std::string& output, std::vector<extTokenId_t>& ids_vec,
bool add_generation_prompt, bool tokenize) const;

private:
OrtxStatus LoadTokenizer(const OrtxTokenizerBlob* blob = nullptr);
Expand Down
100 changes: 57 additions & 43 deletions test/pp_api_test/test_tokenizer_chat.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2257,7 +2257,7 @@ TEST(OrtxTokenizerTest, ChatTemplateDivisionByZero) {
}
}

TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargs) {
TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargsOption) {
OrtxObjectPtr<OrtxTokenizer> tokenizer(OrtxCreateTokenizer, "data/phi-4-base");
ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage();

Expand All @@ -2266,11 +2266,15 @@ TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargs) {
const std::string messages_json = R"([{"role":"user","content":"Hello"}])";
const std::string template_kwargs =
R"({"enable_thinking":false,"reasoning_effort":"low","level":2})";
const char* keys[] = {"chat_template_kwargs"};
const char* values[] = {template_kwargs.c_str()};
ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, values, 1), kOrtxOK)
<< OrtxGetLastErrorMessage();
OrtxObjectPtr<OrtxTensorResult> result;

auto err = OrtxApplyChatTemplateWithOptions(
auto err = OrtxApplyChatTemplate(
tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr,
template_kwargs.c_str(), result.ToBeAssigned(), true, false);
result.ToBeAssigned(), true, false);
ASSERT_EQ(err, kOrtxOK) << OrtxGetLastErrorMessage();

OrtxObjectPtr<OrtxTensor> tensor;
Expand All @@ -2280,7 +2284,7 @@ TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargs) {
EXPECT_STREQ(text, "NO_THINK|low|2");
}

TEST(OrtxTokenizerTest, ChatTemplateKwargsCannotOverrideCoreContext) {
TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCannotOverrideCoreContext) {
OrtxObjectPtr<OrtxTokenizer> tokenizer(OrtxCreateTokenizer, "data/phi-4-base");
ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage();

Expand All @@ -2289,11 +2293,15 @@ TEST(OrtxTokenizerTest, ChatTemplateKwargsCannotOverrideCoreContext) {
const std::string messages_json = R"([{"role":"user","content":"Hello"}])";
const std::string template_kwargs =
R"({"messages":[{"role":"user","content":"Override"}],"add_generation_prompt":false,"tools":[{"name":"override"}]})";
const char* keys[] = {"chat_template_kwargs"};
const char* values[] = {template_kwargs.c_str()};
ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, values, 1), kOrtxOK)
<< OrtxGetLastErrorMessage();
OrtxObjectPtr<OrtxTensorResult> result;

auto err = OrtxApplyChatTemplateWithOptions(
auto err = OrtxApplyChatTemplate(
tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr,
template_kwargs.c_str(), result.ToBeAssigned(), true, false);
result.ToBeAssigned(), true, false);
ASSERT_EQ(err, kOrtxOK) << OrtxGetLastErrorMessage();

OrtxObjectPtr<OrtxTensor> tensor;
Expand All @@ -2303,70 +2311,76 @@ TEST(OrtxTokenizerTest, ChatTemplateKwargsCannotOverrideCoreContext) {
EXPECT_STREQ(text, "Hello|GEN|NO_TOOLS");
}

TEST(OrtxTokenizerTest, ChatTemplateRejectsInvalidTemplateKwargs) {
TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionRejectsInvalidJson) {
OrtxObjectPtr<OrtxTokenizer> tokenizer(OrtxCreateTokenizer, "data/phi-4-base");
ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage();

const std::string messages_json = R"([{"role":"user","content":"Hello"}])";
OrtxObjectPtr<OrtxTensorResult> result;

auto empty_string = OrtxApplyChatTemplateWithOptions(
tokenizer.get(), "{{ messages[0].content }}", messages_json.c_str(), nullptr,
"", result.ToBeAssigned(), true, false);
const char* keys[] = {"chat_template_kwargs"};
const char* empty_value[] = {""};
auto empty_string = OrtxUpdateTokenizerOptions(tokenizer.get(), keys, empty_value, 1);
EXPECT_EQ(empty_string, kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "template_kwargs must be a JSON object or null.");
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Invalid chat_template_kwargs JSON.");

auto invalid_json = OrtxApplyChatTemplateWithOptions(
tokenizer.get(), "{{ messages[0].content }}", messages_json.c_str(), nullptr,
"{", result.ToBeAssigned(), true, false);
const char* invalid_value[] = {"{"};
auto invalid_json = OrtxUpdateTokenizerOptions(tokenizer.get(), keys, invalid_value, 1);
EXPECT_EQ(invalid_json, kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Invalid template_kwargs JSON.");
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Invalid chat_template_kwargs JSON.");

auto non_object = OrtxApplyChatTemplateWithOptions(
tokenizer.get(), "{{ messages[0].content }}", messages_json.c_str(), nullptr,
"[]", result.ToBeAssigned(), true, false);
const char* non_object_value[] = {"[]"};
auto non_object = OrtxUpdateTokenizerOptions(tokenizer.get(), keys, non_object_value, 1);
EXPECT_EQ(non_object, kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "template_kwargs must be a JSON object.");
EXPECT_STREQ(OrtxGetLastErrorMessage(), "chat_template_kwargs must be a JSON object.");
}

TEST(OrtxTokenizerTest, ChatTemplateRejectsNullTokenizerWithExplicitTemplate) {
TEST(OrtxTokenizerTest, ChatTemplateRejectsNullTokenizer) {
const std::string messages_json = R"([{"role":"user","content":"Hello"}])";
OrtxObjectPtr<OrtxTensorResult> result;

auto err = OrtxApplyChatTemplateWithOptions(
auto err = OrtxApplyChatTemplate(
nullptr, "{{ messages[0].content }}", messages_json.c_str(), nullptr,
nullptr, result.ToBeAssigned(), true, false);
result.ToBeAssigned(), true, false);
EXPECT_EQ(err, kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "tokenizer is null");
}

TEST(OrtxTokenizerTest, LegacyChatTemplateApiMatchesNullTemplateKwargs) {
OrtxObjectPtr<OrtxTokenizer> tokenizer(OrtxCreateTokenizer, "data/phi-4-base");
TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCanBeCleared) {
const char* keys[] = {"chat_template_kwargs"};
const char* populated_values[] = {R"({"enable_thinking":false})"};
OrtxObjectPtr<OrtxTokenizer> tokenizer(
OrtxCreateTokenizerWithOptions, "data/phi-4-base", keys, populated_values, 1);
ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage();

const std::string template_str = R"({{ messages[0].content }}|{{ add_generation_prompt }})";
const std::string template_str =
R"({% if enable_thinking is defined %}SET{% else %}UNSET{% endif %}|{{ messages[0].content }})";
const std::string messages_json = R"([{"role":"user","content":"Hello"}])";
OrtxObjectPtr<OrtxTensorResult> legacy_result;
OrtxObjectPtr<OrtxTensorResult> options_result;
OrtxObjectPtr<OrtxTensorResult> configured_result;
OrtxObjectPtr<OrtxTensorResult> empty_options_result;

ASSERT_EQ(OrtxApplyChatTemplate(
tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr,
legacy_result.ToBeAssigned(), true, false),
configured_result.ToBeAssigned(), true, false),
kOrtxOK);
ASSERT_EQ(OrtxApplyChatTemplateWithOptions(
tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr, nullptr,
options_result.ToBeAssigned(), true, false),

const char* cleared_values[] = {"{}"};
ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, cleared_values, 1), kOrtxOK)
<< OrtxGetLastErrorMessage();
ASSERT_EQ(OrtxApplyChatTemplate(
tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr,
empty_options_result.ToBeAssigned(), true, false),
kOrtxOK);

OrtxObjectPtr<OrtxTensor> legacy_tensor;
OrtxObjectPtr<OrtxTensor> options_tensor;
ASSERT_EQ(OrtxTensorResultGetAt(legacy_result.get(), 0, legacy_tensor.ToBeAssigned()), kOrtxOK);
ASSERT_EQ(OrtxTensorResultGetAt(options_result.get(), 0, options_tensor.ToBeAssigned()), kOrtxOK);
const char* legacy_text = nullptr;
const char* options_text = nullptr;
ASSERT_EQ(OrtxGetTensorData(legacy_tensor.get(), reinterpret_cast<const void**>(&legacy_text), nullptr, nullptr),
OrtxObjectPtr<OrtxTensor> configured_tensor;
OrtxObjectPtr<OrtxTensor> empty_options_tensor;
ASSERT_EQ(OrtxTensorResultGetAt(configured_result.get(), 0, configured_tensor.ToBeAssigned()), kOrtxOK);
ASSERT_EQ(OrtxTensorResultGetAt(empty_options_result.get(), 0, empty_options_tensor.ToBeAssigned()), kOrtxOK);
const char* configured_text = nullptr;
const char* empty_options_text = nullptr;
ASSERT_EQ(OrtxGetTensorData(configured_tensor.get(),
reinterpret_cast<const void**>(&configured_text), nullptr, nullptr),
kOrtxOK);
ASSERT_EQ(OrtxGetTensorData(options_tensor.get(), reinterpret_cast<const void**>(&options_text), nullptr, nullptr),
ASSERT_EQ(OrtxGetTensorData(empty_options_tensor.get(),
reinterpret_cast<const void**>(&empty_options_text), nullptr, nullptr),
kOrtxOK);
EXPECT_STREQ(legacy_text, options_text);
EXPECT_STREQ(configured_text, "SET|Hello");
EXPECT_STREQ(empty_options_text, "UNSET|Hello");
}
Loading