From 033ff3051e5640baa4a8f833c46a7b6d26115536 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Sat, 22 Aug 2026 16:17:12 -0700 Subject: [PATCH 1/6] Route chat template kwargs through tokenizer options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317a4c59-c84b-4900-8dc4-01727a1a1b86 --- .github/workflows/branch-cpp-validation.yml | 16 ++++ include/ortx_tokenizer.h | 34 +++----- shared/api/c_api_tokenizer.cc | 27 +++--- shared/api/chat_template.cc | 17 ++-- shared/api/tokenizer_impl.h | 4 +- test/pp_api_test/test_tokenizer_chat.cc | 94 ++++++++++++--------- 6 files changed, 105 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/branch-cpp-validation.yml diff --git a/.github/workflows/branch-cpp-validation.yml b/.github/workflows/branch-cpp-validation.yml new file mode 100644 index 000000000..5f27e9813 --- /dev/null +++ b/.github/workflows/branch-cpp-validation.yml @@ -0,0 +1,16 @@ +name: Branch C++ validation + +on: + push: + branches: + - feature/chat-template-kwargs-update-options + +jobs: + c-api-tests: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Build C API and C++ tests + run: ./build.sh -DOCOS_ENABLE_C_API=ON + - name: Run C++ tests + run: ctest --test-dir out/Linux/RelWithDebInfo -C RelWithDebInfo --output-on-failure diff --git a/include/ortx_tokenizer.h b/include/ortx_tokenizer.h index 53d3b47c1..7a6ba6e63 100644 --- a/include/ortx_tokenizer.h +++ b/include/ortx_tokenizer.h @@ -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. + * * Future tokenizer options may be added without changing this API signature. * * \see OrtxUpdateTokenizerOptions for updating options on an existing tokenizer. @@ -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. * */ @@ -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 diff --git a/shared/api/c_api_tokenizer.cc b/shared/api/c_api_tokenizer.cc index 052f00b62..7d44f77f7 100644 --- a/shared/api/c_api_tokenizer.cc +++ b/shared/api/c_api_tokenizer.cc @@ -4,6 +4,8 @@ #include #include +#include "nlohmann/json.hpp" + #include "c_api_utils.hpp" #include "tokenizer_impl.h" @@ -52,7 +54,8 @@ static std::unordered_map BuildOptionsMap(const char* // Define the set of valid option keys - may be added to in the future static const std::unordered_set valid_keys = { "add_special_tokens", - "skip_special_tokens" + "skip_special_tokens", + "chat_template_kwargs" }; std::unordered_map options; @@ -68,6 +71,18 @@ static std::unordered_map 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 {}; + } + if (!parsed_kwargs.is_object()) { + ReturnableStatus::last_error_message_ = "chat_template_kwargs must be a JSON object."; + return {}; + } + } + options[key] = values[i]; } } @@ -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; @@ -493,7 +500,7 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplateWithOptions(const OrtxTokenizer* t std::string text; std::vector 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(); diff --git a/shared/api/chat_template.cc b/shared/api/chat_template.cc index 23bd8aca9..33de59c16 100644 --- a/shared/api/chat_template.cc +++ b/shared/api/chat_template.cc @@ -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& ids_vec, bool add_generation_prompt, - bool tokenize) const { + std::string& output, std::vector& ids_vec, + bool add_generation_prompt, bool tokenize) const { OrtxStatus status; std::string input_str = minja::normalize_newlines(message); @@ -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); 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); } diff --git a/shared/api/tokenizer_impl.h b/shared/api/tokenizer_impl.h index 7dfd53952..188fc197b 100644 --- a/shared/api/tokenizer_impl.h +++ b/shared/api/tokenizer_impl.h @@ -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>& t_ids) const; OrtxStatus ApplyChatTemplate(const char* template_str, const char* message, const char* tools, - const char* template_kwargs, std::string& output, - std::vector& ids_vec, bool add_generation_prompt, bool tokenize) const; + std::string& output, std::vector& ids_vec, + bool add_generation_prompt, bool tokenize) const; private: OrtxStatus LoadTokenizer(const OrtxTokenizerBlob* blob = nullptr); diff --git a/test/pp_api_test/test_tokenizer_chat.cc b/test/pp_api_test/test_tokenizer_chat.cc index 211a55975..c49cadacc 100644 --- a/test/pp_api_test/test_tokenizer_chat.cc +++ b/test/pp_api_test/test_tokenizer_chat.cc @@ -2257,7 +2257,7 @@ TEST(OrtxTokenizerTest, ChatTemplateDivisionByZero) { } } -TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargs) { +TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargsOption) { OrtxObjectPtr tokenizer(OrtxCreateTokenizer, "data/phi-4-base"); ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage(); @@ -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 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 tensor; @@ -2280,7 +2284,7 @@ TEST(OrtxTokenizerTest, ChatTemplateAcceptsTypedTemplateKwargs) { EXPECT_STREQ(text, "NO_THINK|low|2"); } -TEST(OrtxTokenizerTest, ChatTemplateKwargsCannotOverrideCoreContext) { +TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCannotOverrideCoreContext) { OrtxObjectPtr tokenizer(OrtxCreateTokenizer, "data/phi-4-base"); ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage(); @@ -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 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 tensor; @@ -2303,70 +2311,74 @@ TEST(OrtxTokenizerTest, ChatTemplateKwargsCannotOverrideCoreContext) { EXPECT_STREQ(text, "Hello|GEN|NO_TOOLS"); } -TEST(OrtxTokenizerTest, ChatTemplateRejectsInvalidTemplateKwargs) { +TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionRejectsInvalidJson) { OrtxObjectPtr tokenizer(OrtxCreateTokenizer, "data/phi-4-base"); ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage(); - const std::string messages_json = R"([{"role":"user","content":"Hello"}])"; - OrtxObjectPtr 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 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) { +TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCanBeCleared) { OrtxObjectPtr tokenizer(OrtxCreateTokenizer, "data/phi-4-base"); ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage(); const std::string template_str = R"({{ messages[0].content }}|{{ add_generation_prompt }})"; const std::string messages_json = R"([{"role":"user","content":"Hello"}])"; - OrtxObjectPtr legacy_result; - OrtxObjectPtr options_result; + OrtxObjectPtr default_result; + OrtxObjectPtr empty_options_result; ASSERT_EQ(OrtxApplyChatTemplate( tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr, - legacy_result.ToBeAssigned(), true, false), + default_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* keys[] = {"chat_template_kwargs"}; + const char* populated_values[] = {R"({"enable_thinking":false})"}; + ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, populated_values, 1), kOrtxOK) + << OrtxGetLastErrorMessage(); + 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 legacy_tensor; - OrtxObjectPtr 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(&legacy_text), nullptr, nullptr), + OrtxObjectPtr default_tensor; + OrtxObjectPtr empty_options_tensor; + ASSERT_EQ(OrtxTensorResultGetAt(default_result.get(), 0, default_tensor.ToBeAssigned()), kOrtxOK); + ASSERT_EQ(OrtxTensorResultGetAt(empty_options_result.get(), 0, empty_options_tensor.ToBeAssigned()), kOrtxOK); + const char* default_text = nullptr; + const char* empty_options_text = nullptr; + ASSERT_EQ(OrtxGetTensorData(default_tensor.get(), reinterpret_cast(&default_text), nullptr, nullptr), kOrtxOK); - ASSERT_EQ(OrtxGetTensorData(options_tensor.get(), reinterpret_cast(&options_text), nullptr, nullptr), + ASSERT_EQ(OrtxGetTensorData(empty_options_tensor.get(), + reinterpret_cast(&empty_options_text), nullptr, nullptr), kOrtxOK); - EXPECT_STREQ(legacy_text, options_text); + EXPECT_STREQ(default_text, empty_options_text); } \ No newline at end of file From 2d2b4bfb09d39434ac6329ea2d0c25a436e27dc2 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Sat, 22 Aug 2026 16:41:23 -0700 Subject: [PATCH 2/6] Remove temporary branch validation workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317a4c59-c84b-4900-8dc4-01727a1a1b86 --- .github/workflows/branch-cpp-validation.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/branch-cpp-validation.yml diff --git a/.github/workflows/branch-cpp-validation.yml b/.github/workflows/branch-cpp-validation.yml deleted file mode 100644 index 5f27e9813..000000000 --- a/.github/workflows/branch-cpp-validation.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Branch C++ validation - -on: - push: - branches: - - feature/chat-template-kwargs-update-options - -jobs: - c-api-tests: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Build C API and C++ tests - run: ./build.sh -DOCOS_ENABLE_C_API=ON - - name: Run C++ tests - run: ctest --test-dir out/Linux/RelWithDebInfo -C RelWithDebInfo --output-on-failure From f76e08629f55764e3dd639030c1845ab988aadc6 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Sat, 22 Aug 2026 18:11:06 -0700 Subject: [PATCH 3/6] Complete tokenizer option coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317a4c59-c84b-4900-8dc4-01727a1a1b86 --- .github/workflows/branch-cpp-validation.yml | 16 ++++++++++++ pyop/py_c_api.cc | 4 +-- test/pp_api_test/test_tokenizer_chat.cc | 28 +++++++++++---------- 3 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/branch-cpp-validation.yml diff --git a/.github/workflows/branch-cpp-validation.yml b/.github/workflows/branch-cpp-validation.yml new file mode 100644 index 000000000..5f27e9813 --- /dev/null +++ b/.github/workflows/branch-cpp-validation.yml @@ -0,0 +1,16 @@ +name: Branch C++ validation + +on: + push: + branches: + - feature/chat-template-kwargs-update-options + +jobs: + c-api-tests: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Build C API and C++ tests + run: ./build.sh -DOCOS_ENABLE_C_API=ON + - name: Run C++ tests + run: ctest --test-dir out/Linux/RelWithDebInfo -C RelWithDebInfo --output-on-failure diff --git a/pyop/py_c_api.cc b/pyop/py_c_api.cc index 27e10d196..19d68f33a 100644 --- a/pyop/py_c_api.cc +++ b/pyop/py_c_api.cc @@ -165,7 +165,7 @@ void AddGlobalMethodsCApi(pybind11::module& m) { return reinterpret_cast(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", @@ -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", diff --git a/test/pp_api_test/test_tokenizer_chat.cc b/test/pp_api_test/test_tokenizer_chat.cc index c49cadacc..cf4f08628 100644 --- a/test/pp_api_test/test_tokenizer_chat.cc +++ b/test/pp_api_test/test_tokenizer_chat.cc @@ -2344,23 +2344,23 @@ TEST(OrtxTokenizerTest, ChatTemplateRejectsNullTokenizer) { } TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCanBeCleared) { - OrtxObjectPtr tokenizer(OrtxCreateTokenizer, "data/phi-4-base"); + const char* keys[] = {"chat_template_kwargs"}; + const char* populated_values[] = {R"({"enable_thinking":false})"}; + OrtxObjectPtr 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 default_result; + OrtxObjectPtr configured_result; OrtxObjectPtr empty_options_result; ASSERT_EQ(OrtxApplyChatTemplate( tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr, - default_result.ToBeAssigned(), true, false), + configured_result.ToBeAssigned(), true, false), kOrtxOK); - const char* keys[] = {"chat_template_kwargs"}; - const char* populated_values[] = {R"({"enable_thinking":false})"}; - ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, populated_values, 1), kOrtxOK) - << OrtxGetLastErrorMessage(); const char* cleared_values[] = {"{}"}; ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, cleared_values, 1), kOrtxOK) << OrtxGetLastErrorMessage(); @@ -2369,16 +2369,18 @@ TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCanBeCleared) { empty_options_result.ToBeAssigned(), true, false), kOrtxOK); - OrtxObjectPtr default_tensor; + OrtxObjectPtr configured_tensor; OrtxObjectPtr empty_options_tensor; - ASSERT_EQ(OrtxTensorResultGetAt(default_result.get(), 0, default_tensor.ToBeAssigned()), kOrtxOK); + 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* default_text = nullptr; + const char* configured_text = nullptr; const char* empty_options_text = nullptr; - ASSERT_EQ(OrtxGetTensorData(default_tensor.get(), reinterpret_cast(&default_text), nullptr, nullptr), + ASSERT_EQ(OrtxGetTensorData(configured_tensor.get(), + reinterpret_cast(&configured_text), nullptr, nullptr), kOrtxOK); ASSERT_EQ(OrtxGetTensorData(empty_options_tensor.get(), reinterpret_cast(&empty_options_text), nullptr, nullptr), kOrtxOK); - EXPECT_STREQ(default_text, empty_options_text); + EXPECT_STREQ(configured_text, "SET|Hello"); + EXPECT_STREQ(empty_options_text, "UNSET|Hello"); } \ No newline at end of file From f9540c0dbb2dee1275f96213b3ba321503d67a98 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Sat, 22 Aug 2026 18:15:46 -0700 Subject: [PATCH 4/6] Remove temporary branch validation workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317a4c59-c84b-4900-8dc4-01727a1a1b86 --- .github/workflows/branch-cpp-validation.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/branch-cpp-validation.yml diff --git a/.github/workflows/branch-cpp-validation.yml b/.github/workflows/branch-cpp-validation.yml deleted file mode 100644 index 5f27e9813..000000000 --- a/.github/workflows/branch-cpp-validation.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Branch C++ validation - -on: - push: - branches: - - feature/chat-template-kwargs-update-options - -jobs: - c-api-tests: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Build C API and C++ tests - run: ./build.sh -DOCOS_ENABLE_C_API=ON - - name: Run C++ tests - run: ctest --test-dir out/Linux/RelWithDebInfo -C RelWithDebInfo --output-on-failure From e6718ba32a44a31647fc76000a0b211b93e42a86 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Sat, 22 Aug 2026 18:48:50 -0700 Subject: [PATCH 5/6] Address tokenizer option review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317a4c59-c84b-4900-8dc4-01727a1a1b86 --- .github/workflows/branch-cpp-validation.yml | 16 ++ include/ortx_tokenizer.h | 23 +++ shared/api/c_api_tokenizer.cc | 159 +++++++++++++------- shared/api/chat_template.cc | 17 ++- shared/api/tokenizer_impl.cc | 16 +- shared/api/tokenizer_impl.h | 12 +- test/pp_api_test/test_tokenizer_capi.cc | 21 +++ test/pp_api_test/test_tokenizer_chat.cc | 39 +++++ 8 files changed, 238 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/branch-cpp-validation.yml diff --git a/.github/workflows/branch-cpp-validation.yml b/.github/workflows/branch-cpp-validation.yml new file mode 100644 index 000000000..5f27e9813 --- /dev/null +++ b/.github/workflows/branch-cpp-validation.yml @@ -0,0 +1,16 @@ +name: Branch C++ validation + +on: + push: + branches: + - feature/chat-template-kwargs-update-options + +jobs: + c-api-tests: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Build C API and C++ tests + run: ./build.sh -DOCOS_ENABLE_C_API=ON + - name: Run C++ tests + run: ctest --test-dir out/Linux/RelWithDebInfo -C RelWithDebInfo --output-on-failure diff --git a/include/ortx_tokenizer.h b/include/ortx_tokenizer.h index 7a6ba6e63..ccc10fd55 100644 --- a/include/ortx_tokenizer.h +++ b/include/ortx_tokenizer.h @@ -286,6 +286,29 @@ 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 per-call template context values. + * + * 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. + * @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 diff --git a/shared/api/c_api_tokenizer.cc b/shared/api/c_api_tokenizer.cc index 7d44f77f7..c92a231db 100644 --- a/shared/api/c_api_tokenizer.cc +++ b/shared/api/c_api_tokenizer.cc @@ -60,31 +60,46 @@ static std::unordered_map BuildOptionsMap(const char* std::unordered_map 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 (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 {}; - } + 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; @@ -92,16 +107,14 @@ static std::unordered_map BuildOptionsMap(const char* // Helper function to parse boolean tokenizer options static bool ParseBoolOption( - const std::unordered_map& options_map, - const std::string& option_name, + const std::optional& 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") { @@ -140,8 +153,12 @@ extError_t ORTX_API_CALL OrtxCreateTokenizerWithOptions( auto ptr = std::make_unique(); - // 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); @@ -222,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()) { @@ -303,7 +320,7 @@ extError_t ORTX_API_CALL OrtxDetokenize(const OrtxTokenizer* tokenizer, const Or std::vector 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()) { @@ -334,7 +351,7 @@ extError_t ORTX_API_CALL OrtxDetokenize1D(const OrtxTokenizer* tokenizer, const std::vector 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()) { @@ -467,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)); @@ -478,30 +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) { - 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(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 ids_vec; - status = token_ptr->ApplyChatTemplate(template_str, input, tools, 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(); std::vector> tensors; @@ -518,3 +518,58 @@ extError_t ORTX_API_CALL OrtxApplyChatTemplate(const OrtxTokenizer* tokenizer, c 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(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); +} diff --git a/shared/api/chat_template.cc b/shared/api/chat_template.cc index 33de59c16..23bd8aca9 100644 --- a/shared/api/chat_template.cc +++ b/shared/api/chat_template.cc @@ -376,8 +376,9 @@ std::string normalize_tool_quotes(const std::string& input) { } OrtxStatus TokenizerImpl::ApplyChatTemplate(const char* template_str, const char* message, const char* tools, - std::string& output, std::vector& ids_vec, - bool add_generation_prompt, bool tokenize) const { + const char* template_kwargs, std::string& output, + std::vector& ids_vec, bool add_generation_prompt, + bool tokenize) const { OrtxStatus status; std::string input_str = minja::normalize_newlines(message); @@ -409,15 +410,17 @@ OrtxStatus TokenizerImpl::ApplyChatTemplate(const char* template_str, const char } json context_values = json::object(); - 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, + 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, /*allow_exceptions=*/false); if (parsed_kwargs.is_discarded()) { - throw std::runtime_error("Invalid chat_template_kwargs JSON."); + throw std::runtime_error("Invalid template_kwargs JSON."); } if (!parsed_kwargs.is_object()) { - throw std::runtime_error("chat_template_kwargs must be a JSON object."); + throw std::runtime_error("template_kwargs must be a JSON object."); } context_values = std::move(parsed_kwargs); } diff --git a/shared/api/tokenizer_impl.cc b/shared/api/tokenizer_impl.cc index b5b064f61..0360f095f 100644 --- a/shared/api/tokenizer_impl.cc +++ b/shared/api/tokenizer_impl.cc @@ -116,14 +116,26 @@ OrtxStatus TokenizerImpl::UpdateOptions(const std::unordered_map lock(options_mutex_); + for (const auto& [k, v] : options) { + options_map_[k] = v; } return OrtxStatus(kOrtxOK, "Tokenizer options updated successfully."); } +std::optional TokenizerImpl::GetOption(const std::string& name) const { + std::lock_guard 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& input, std::vector>& t_ids, bool add_special_tokens) const { diff --git a/shared/api/tokenizer_impl.h b/shared/api/tokenizer_impl.h index 188fc197b..90ec990cd 100644 --- a/shared/api/tokenizer_impl.h +++ b/shared/api/tokenizer_impl.h @@ -3,6 +3,8 @@ #pragma once +#include +#include #include #include "bpe_kernels.h" @@ -24,6 +26,7 @@ class TokenizerImpl : public OrtxObjectImpl { OrtxStatus Load(const OrtxTokenizerBlob& blob); OrtxStatus UpdateOptions(const std::unordered_map& options); + std::optional GetOption(const std::string& name) const; OrtxStatus Tokenize(const std::vector& input, std::vector>& t_ids, bool add_special_tokens = true) const { return BatchEncode(input, t_ids, add_special_tokens); @@ -64,8 +67,6 @@ class TokenizerImpl : public OrtxObjectImpl { mutable std::string tool_calls; - std::unordered_map options_map; - std::string bos_token; std::string eos_token; std::vector custom_tools; @@ -90,10 +91,13 @@ class TokenizerImpl : public OrtxObjectImpl { OrtxStatus GetDecoderPromptIds(size_t batch_size, const char* lang, const char* task, int no_timestamps, std::vector>& t_ids) const; OrtxStatus ApplyChatTemplate(const char* template_str, const char* message, const char* tools, - std::string& output, std::vector& ids_vec, - bool add_generation_prompt, bool tokenize) const; + const char* template_kwargs, std::string& output, + std::vector& ids_vec, bool add_generation_prompt, bool tokenize) const; private: + mutable std::mutex options_mutex_; + std::unordered_map options_map_; + OrtxStatus LoadTokenizer(const OrtxTokenizerBlob* blob = nullptr); OrtxStatus LoadChatTemplate(); diff --git a/test/pp_api_test/test_tokenizer_capi.cc b/test/pp_api_test/test_tokenizer_capi.cc index 9535a5065..e698e6c68 100644 --- a/test/pp_api_test/test_tokenizer_capi.cc +++ b/test/pp_api_test/test_tokenizer_capi.cc @@ -28,6 +28,27 @@ TEST(CApiTest, ApiTest) { free(decoded_text); } +TEST(OrtxTokenizerTest, TokenizerOptionsRejectNullArguments) { + OrtxObjectPtr 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"); diff --git a/test/pp_api_test/test_tokenizer_chat.cc b/test/pp_api_test/test_tokenizer_chat.cc index cf4f08628..f2f881564 100644 --- a/test/pp_api_test/test_tokenizer_chat.cc +++ b/test/pp_api_test/test_tokenizer_chat.cc @@ -2383,4 +2383,43 @@ TEST(OrtxTokenizerTest, ChatTemplateKwargsOptionCanBeCleared) { kOrtxOK); EXPECT_STREQ(configured_text, "SET|Hello"); EXPECT_STREQ(empty_options_text, "UNSET|Hello"); +} + +TEST(OrtxTokenizerTest, ChatTemplateWithOptionsRemainsPerCall) { + OrtxObjectPtr tokenizer(OrtxCreateTokenizer, "data/phi-4-base"); + ASSERT_EQ(tokenizer.Code(), kOrtxOK) << OrtxGetLastErrorMessage(); + + const char* keys[] = {"chat_template_kwargs"}; + const char* persistent_values[] = {R"({"mode":"persistent"})"}; + ASSERT_EQ(OrtxUpdateTokenizerOptions(tokenizer.get(), keys, persistent_values, 1), kOrtxOK) + << OrtxGetLastErrorMessage(); + + const std::string template_str = R"({{ mode }})"; + const std::string messages_json = R"([{"role":"user","content":"Hello"}])"; + OrtxObjectPtr per_call_result; + ASSERT_EQ(OrtxApplyChatTemplateWithOptions( + tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr, + R"({"mode":"per-call"})", per_call_result.ToBeAssigned(), true, false), + kOrtxOK); + + OrtxObjectPtr persistent_result; + ASSERT_EQ(OrtxApplyChatTemplate( + tokenizer.get(), template_str.c_str(), messages_json.c_str(), nullptr, + persistent_result.ToBeAssigned(), true, false), + kOrtxOK); + + OrtxObjectPtr per_call_tensor; + OrtxObjectPtr persistent_tensor; + ASSERT_EQ(OrtxTensorResultGetAt(per_call_result.get(), 0, per_call_tensor.ToBeAssigned()), kOrtxOK); + ASSERT_EQ(OrtxTensorResultGetAt(persistent_result.get(), 0, persistent_tensor.ToBeAssigned()), kOrtxOK); + const char* per_call_text = nullptr; + const char* persistent_text = nullptr; + ASSERT_EQ(OrtxGetTensorData(per_call_tensor.get(), + reinterpret_cast(&per_call_text), nullptr, nullptr), + kOrtxOK); + ASSERT_EQ(OrtxGetTensorData(persistent_tensor.get(), + reinterpret_cast(&persistent_text), nullptr, nullptr), + kOrtxOK); + EXPECT_STREQ(per_call_text, "per-call"); + EXPECT_STREQ(persistent_text, "persistent"); } \ No newline at end of file From 3a46e6949c5037a929ee1eb71381d92bc58f7308 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Sat, 22 Aug 2026 18:54:12 -0700 Subject: [PATCH 6/6] Remove temporary branch validation workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317a4c59-c84b-4900-8dc4-01727a1a1b86 --- .github/workflows/branch-cpp-validation.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/branch-cpp-validation.yml diff --git a/.github/workflows/branch-cpp-validation.yml b/.github/workflows/branch-cpp-validation.yml deleted file mode 100644 index 5f27e9813..000000000 --- a/.github/workflows/branch-cpp-validation.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Branch C++ validation - -on: - push: - branches: - - feature/chat-template-kwargs-update-options - -jobs: - c-api-tests: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Build C API and C++ tests - run: ./build.sh -DOCOS_ENABLE_C_API=ON - - name: Run C++ tests - run: ctest --test-dir out/Linux/RelWithDebInfo -C RelWithDebInfo --output-on-failure