Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 14 additions & 5 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 @@ -277,12 +287,11 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplate(const OrtxTokenizer* tokenizer, c
bool add_generation_prompt, bool tokenize);

/**
* @brief Applies a chat template with additional template context values.
* @brief Applies a chat template with per-call 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.
* This compatibility API preserves the per-call behavior introduced in #1102.
* New callers should prefer configuring `chat_template_kwargs` through
* OrtxUpdateTokenizerOptions and then calling OrtxApplyChatTemplate.
*
* @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.
Expand Down
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
162 changes: 112 additions & 50 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,41 +54,67 @@ 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;

if (num_options > 0 && keys == nullptr) {
ReturnableStatus::last_error_message_ = "Tokenizer option keys array is null.";
return {};
}
if (num_options > 0 && values == nullptr) {
ReturnableStatus::last_error_message_ = "Tokenizer option values array is null.";
return {};
}

for (size_t i = 0; i < num_options; ++i) {
if (keys[i] && values[i]) {
std::string key = keys[i];
if (keys[i] == nullptr) {
ReturnableStatus::last_error_message_ = "Tokenizer option key at index " + std::to_string(i) + " is null.";
return {};
}
if (values[i] == nullptr) {
ReturnableStatus::last_error_message_ = "Tokenizer option value at index " + std::to_string(i) + " is null.";
return {};
}

std::string key = keys[i];

if (valid_keys.find(key) == valid_keys.end()) {
ReturnableStatus::last_error_message_ =
"Invalid tokenizer option key: " + key;
return {};
}

if (valid_keys.find(key) == valid_keys.end()) {
ReturnableStatus::last_error_message_ =
"Invalid tokenizer option key: " + key;
// Return empty map — caller should handle this as an error
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 {};
}
if (!parsed_kwargs.is_object()) {
ReturnableStatus::last_error_message_ = "chat_template_kwargs must be a JSON object.";
return {};
}

options[key] = values[i];
}

options[key] = values[i];
}

return options;
}

// Helper function to parse boolean tokenizer options
static bool ParseBoolOption(
const std::unordered_map<std::string, std::string>& options_map,
const std::string& option_name,
const std::optional<std::string>& option,
bool default_value = true)
{
auto it = options_map.find(option_name);
if (it == options_map.end()) {
if (!option.has_value()) {
return default_value;
}

std::string val = it->second;
std::string val = *option;
std::transform(val.begin(), val.end(), val.begin(), ::tolower);

if (val == "false" || val == "0") {
Expand Down Expand Up @@ -125,8 +153,12 @@ extError_t ORTX_API_CALL OrtxCreateTokenizerWithOptions(

auto ptr = std::make_unique<ort_extensions::TokenizerImpl>();

// Initialize tokenizer options
ptr->options_map = std::move(options);
if (!options.empty()) {
status = ptr->UpdateOptions(options);
if (!status.IsOk()) {
return status.Code();
}
}

status = ptr->Load(tokenizer_path);

Expand Down Expand Up @@ -207,7 +239,7 @@ extError_t ORTX_API_CALL OrtxTokenize(const OrtxTokenizer* tokenizer, const char
[](const char* str) { return std::string_view(str); });

// If add_special_tokens option exists, use its value, otherwise use default (true)
bool add_special_tokens = ParseBoolOption(token_ptr->options_map, "add_special_tokens", true);
bool add_special_tokens = ParseBoolOption(token_ptr->GetOption("add_special_tokens"), true);
status = token_ptr->Tokenize(input_view, t_ids, add_special_tokens);

if (!status.IsOk()) {
Expand Down Expand Up @@ -288,7 +320,7 @@ extError_t ORTX_API_CALL OrtxDetokenize(const OrtxTokenizer* tokenizer, const Or
std::vector<std::string> output_text;

// If skip_special_tokens option exists, use its value, otherwise use default (true)
bool skip_special_tokens = ParseBoolOption(token_ptr->options_map, "skip_special_tokens", true);
bool skip_special_tokens = ParseBoolOption(token_ptr->GetOption("skip_special_tokens"), true);
status = token_ptr->Detokenize(t_ids, output_text, skip_special_tokens);

if (!status.IsOk()) {
Expand Down Expand Up @@ -319,7 +351,7 @@ extError_t ORTX_API_CALL OrtxDetokenize1D(const OrtxTokenizer* tokenizer, const
std::vector<std::string> output_text;

// If skip_special_tokens option exists, use its value, otherwise use default (true)
bool skip_special_tokens = ParseBoolOption(token_ptr->options_map, "skip_special_tokens", true);
bool skip_special_tokens = ParseBoolOption(token_ptr->GetOption("skip_special_tokens"), true);
status = token_ptr->Detokenize(t_ids, output_text, skip_special_tokens);

if (!status.IsOk()) {
Expand Down Expand Up @@ -452,7 +484,7 @@ extError_t ORTX_API_CALL OrtxDetokenizeCached(const OrtxTokenizer* tokenizer, Or
cache_ptr->last_text_.clear();

// If skip_special_tokens option exists, use its value, otherwise use default (true)
bool skip_special_tokens = ParseBoolOption(token_ptr->options_map, "skip_special_tokens", true);
bool skip_special_tokens = ParseBoolOption(token_ptr->GetOption("skip_special_tokens"), true);
status = ReturnableStatus(token_ptr->Id2Token(next_id, cache_ptr->last_text_,
cache_ptr->decoder_state_, skip_special_tokens));

Expand All @@ -463,38 +495,13 @@ extError_t ORTX_API_CALL OrtxDetokenizeCached(const OrtxTokenizer* tokenizer, Or
return status.Code();
}

extError_t ORTX_API_CALL OrtxApplyChatTemplate(const OrtxTokenizer* tokenizer, const char* template_str,
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;
}

if (input == nullptr || output == nullptr) {
ReturnableStatus::last_error_message_ = "Invalid argument";
return kOrtxErrorInvalidArgument;
}

const auto token_ptr = static_cast<const TokenizerImpl*>(tokenizer);
ReturnableStatus status(token_ptr->IsInstanceOf(extObjectKind_t::kOrtxKindTokenizer));
if (!status.IsOk()) {
return status.Code();
}

static extError_t ApplyChatTemplateImpl(const TokenizerImpl* token_ptr, const char* template_str,
const char* input, const char* tools, const char* template_kwargs,
OrtxTensorResult** output, bool add_generation_prompt, bool tokenize) {
std::string text;
std::vector<extTokenId_t> ids_vec;
status = token_ptr->ApplyChatTemplate(template_str, input, tools, template_kwargs, text, ids_vec,
add_generation_prompt, tokenize);
ReturnableStatus status = token_ptr->ApplyChatTemplate(template_str, input, tools, template_kwargs, text, ids_vec,
add_generation_prompt, tokenize);
if (status.IsOk()) {
auto result = std::make_unique<ort_extensions::TensorResult>();
std::vector<std::unique_ptr<ortc::TensorBase>> tensors;
Expand All @@ -511,3 +518,58 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplateWithOptions(const OrtxTokenizer* t

return status.Code();
}

static const TokenizerImpl* ValidateChatTemplateArguments(const OrtxTokenizer* tokenizer, const char* input,
OrtxTensorResult** output, extError_t& error) {
if (tokenizer == nullptr) {
ReturnableStatus::last_error_message_ = "tokenizer is null";
error = kOrtxErrorInvalidArgument;
return nullptr;
}

if (input == nullptr || output == nullptr) {
ReturnableStatus::last_error_message_ = "Invalid argument";
error = kOrtxErrorInvalidArgument;
return nullptr;
}

const auto token_ptr = static_cast<const TokenizerImpl*>(tokenizer);
ReturnableStatus status(token_ptr->IsInstanceOf(extObjectKind_t::kOrtxKindTokenizer));
if (!status.IsOk()) {
error = status.Code();
return nullptr;
}

error = kOrtxOK;
return token_ptr;
}

extError_t ORTX_API_CALL OrtxApplyChatTemplate(const OrtxTokenizer* tokenizer, const char* template_str,
const char* input, const char* tools,
OrtxTensorResult** output, bool add_generation_prompt,
bool tokenize) {
extError_t error;
const auto token_ptr = ValidateChatTemplateArguments(tokenizer, input, output, error);
if (!token_ptr) {
return error;
}

const auto template_kwargs = token_ptr->GetOption("chat_template_kwargs");
return ApplyChatTemplateImpl(token_ptr, template_str, input, tools,
template_kwargs ? template_kwargs->c_str() : 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) {
extError_t error;
const auto token_ptr = ValidateChatTemplateArguments(tokenizer, input, output, error);
if (!token_ptr) {
return error;
}

return ApplyChatTemplateImpl(token_ptr, template_str, input, tools, template_kwargs, output,
add_generation_prompt, tokenize);
}
16 changes: 14 additions & 2 deletions shared/api/tokenizer_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,26 @@ OrtxStatus TokenizerImpl::UpdateOptions(const std::unordered_map<std::string, st
if (v.empty()) {
return OrtxStatus(kOrtxErrorInvalidArgument, "Option value cannot be empty for key: " + k);
}
}

// Insert new or update existing KV pair
options_map[k] = v;
std::lock_guard<std::mutex> lock(options_mutex_);
for (const auto& [k, v] : options) {
options_map_[k] = v;
}

return OrtxStatus(kOrtxOK, "Tokenizer options updated successfully.");
}

std::optional<std::string> TokenizerImpl::GetOption(const std::string& name) const {
std::lock_guard<std::mutex> lock(options_mutex_);
const auto option = options_map_.find(name);
if (option == options_map_.end()) {
return std::nullopt;
}

return option->second;
}

OrtxStatus TokenizerImpl::BatchEncode(const std::vector<std::string_view>& input,
std::vector<std::vector<extTokenId_t>>& t_ids,
bool add_special_tokens) const {
Expand Down
8 changes: 6 additions & 2 deletions shared/api/tokenizer_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#pragma once

#include <mutex>
#include <optional>
#include <variant>

#include "bpe_kernels.h"
Expand All @@ -24,6 +26,7 @@ class TokenizerImpl : public OrtxObjectImpl {
OrtxStatus Load(const OrtxTokenizerBlob& blob);

OrtxStatus UpdateOptions(const std::unordered_map<std::string, std::string>& options);
std::optional<std::string> GetOption(const std::string& name) const;

OrtxStatus Tokenize(const std::vector<std::string_view>& input, std::vector<std::vector<extTokenId_t>>& t_ids, bool add_special_tokens = true) const {
return BatchEncode(input, t_ids, add_special_tokens);
Expand Down Expand Up @@ -64,8 +67,6 @@ class TokenizerImpl : public OrtxObjectImpl {

mutable std::string tool_calls;

std::unordered_map<std::string, std::string> options_map;

std::string bos_token;
std::string eos_token;
std::vector<std::string> custom_tools;
Expand Down Expand Up @@ -94,6 +95,9 @@ class TokenizerImpl : public OrtxObjectImpl {
std::vector<extTokenId_t>& ids_vec, bool add_generation_prompt, bool tokenize) const;

private:
mutable std::mutex options_mutex_;
std::unordered_map<std::string, std::string> options_map_;

OrtxStatus LoadTokenizer(const OrtxTokenizerBlob* blob = nullptr);
OrtxStatus LoadChatTemplate();

Expand Down
21 changes: 21 additions & 0 deletions test/pp_api_test/test_tokenizer_capi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ TEST(CApiTest, ApiTest) {
free(decoded_text);
}

TEST(OrtxTokenizerTest, TokenizerOptionsRejectNullArguments) {
OrtxObjectPtr<OrtxTokenizer> tokenizer(OrtxCreateTokenizer, "data/llama2");
ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage();

const char* keys[] = {"add_special_tokens"};
const char* values[] = {"false"};
EXPECT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), nullptr, values, 1), kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Tokenizer option keys array is null.");

EXPECT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, nullptr, 1), kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Tokenizer option values array is null.");

const char* null_keys[] = {nullptr};
EXPECT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), null_keys, values, 1), kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Tokenizer option key at index 0 is null.");

const char* null_values[] = {nullptr};
EXPECT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, null_values, 1), kOrtxErrorInvalidArgument);
EXPECT_STREQ(OrtxGetLastErrorMessage(), "Tokenizer option value at index 0 is null.");
}

TEST(CApiTest, StreamApiTest) {
OrtxTokenizer* tokenizer = NULL;
extError_t err = OrtxCreate(kOrtxKindTokenizer, &tokenizer, "data/llama2");
Expand Down
Loading
Loading