diff --git a/include/contracts/config_schema_validation.h b/include/contracts/config_schema_validation.h index 77d771a6..478f3240 100644 --- a/include/contracts/config_schema_validation.h +++ b/include/contracts/config_schema_validation.h @@ -10,6 +10,7 @@ #include "contracts/config_schema.h" #include "contracts/error_codes.h" +#include "contracts/json_pointer.h" namespace llm_edgeflow { @@ -208,19 +209,6 @@ inline bool ValidateConfigFieldDefinitions( namespace detail { -inline std::string ConfigPointerToken(const std::string& value) { - std::string escaped; - for (char c : value) { - if (c == '~') - escaped += "~0"; - else if (c == '/') - escaped += "~1"; - else - escaped += c; - } - return escaped; -} - inline bool ConfigError(std::vector* errors, const std::string& path, ConfigFieldErrorKind kind, std::string message) { @@ -242,14 +230,14 @@ inline bool NormalizeConfigObject( std::find_if(fields.begin(), fields.end(), [&](const auto& f) { return f.name == it.key(); }); if (found == fields.end()) { - ConfigError(errors, path + "/" + ConfigPointerToken(it.key()), + ConfigError(errors, path + "/" + EscapeJsonPointer(it.key()), ConfigFieldErrorKind::kUnknownField, "Unknown config field: " + it.key()); ok = false; } } for (const auto& field : fields) { - const auto field_path = path + "/" + ConfigPointerToken(field.name); + const auto field_path = path + "/" + EscapeJsonPointer(field.name); const auto entry = input.find(field.name); if (entry == input.end() && field.required) { ConfigError(errors, field_path, ConfigFieldErrorKind::kMissingField, @@ -334,7 +322,7 @@ inline bool NormalizeConfigValue( bool ok = true; for (auto it = value.begin(); it != value.end(); ++it) { const auto key = value.is_array() ? std::to_string(it - value.begin()) - : ConfigPointerToken(it.key()); + : EscapeJsonPointer(it.key()); nlohmann::json next; if (!NormalizeConfigValue(*field.items, *it, &next, errors, path + "/" + key)) { diff --git a/include/contracts/parameters.h b/include/contracts/parameters.h index 8c02f76e..b0a7d24c 100644 --- a/include/contracts/parameters.h +++ b/include/contracts/parameters.h @@ -876,8 +876,8 @@ class Parameters { ParamsT params{}; for (const auto& binding : bindings_) { if (!binding->Assign(normalized, ¶ms, error)) { - SetDiagnosticNoexcept( - field_path, "/" + detail::ConfigPointerToken(binding->Name())); + SetDiagnosticNoexcept(field_path, + "/" + EscapeJsonPointer(binding->Name())); return std::nullopt; } } diff --git a/src/engine/backends/inferlet/inferlet_backend.cpp b/src/engine/backends/inferlet/inferlet_backend.cpp index e23062d7..51f38276 100644 --- a/src/engine/backends/inferlet/inferlet_backend.cpp +++ b/src/engine/backends/inferlet/inferlet_backend.cpp @@ -257,11 +257,9 @@ class InferletSession : public Interface { static_cast(decoded.size()), &text_size, 0), "Decode"); decoded.resize(static_cast(text_size)); - size_t stop_position = decoded.size(); - for (const auto& word : options.stop_words) { - stop_position = std::min(stop_position, decoded.find(word)); - } - decoded.resize(stop_position); + const auto stop_position = + text_generation::FindFirstStop(decoded, options.stop_words); + if (stop_position != std::string::npos) decoded.resize(stop_position); *output = std::move(decoded); } utf8::StripIncompleteSuffix(output); diff --git a/src/engine/backends/kite_llm/kite_llm_backend.cpp b/src/engine/backends/kite_llm/kite_llm_backend.cpp index 50863c33..947386aa 100644 --- a/src/engine/backends/kite_llm/kite_llm_backend.cpp +++ b/src/engine/backends/kite_llm/kite_llm_backend.cpp @@ -249,11 +249,9 @@ class KiteSession : public Interface { static_cast(decoded.size()), &text_size, 0), "Decode"); decoded.resize(static_cast(text_size)); - size_t stop_position = decoded.size(); - for (const auto& word : options.stop_words) { - stop_position = std::min(stop_position, decoded.find(word)); - } - decoded.resize(stop_position); + const auto stop_position = + text_generation::FindFirstStop(decoded, options.stop_words); + if (stop_position != std::string::npos) decoded.resize(stop_position); *output = std::move(decoded); } utf8::StripIncompleteSuffix(output); diff --git a/src/engine/text_generation/common_autoregressive_generator.cpp b/src/engine/text_generation/common_autoregressive_generator.cpp index 13f2f3a5..2defb9e2 100644 --- a/src/engine/text_generation/common_autoregressive_generator.cpp +++ b/src/engine/text_generation/common_autoregressive_generator.cpp @@ -142,6 +142,8 @@ bool SampleToken(const std::vector& source_logits, return true; } +} // namespace + size_t FindFirstStop(const std::string& text, const std::vector& stop_words) noexcept { size_t first = std::string::npos; @@ -155,8 +157,6 @@ size_t FindFirstStop(const std::string& text, return first; } -} // namespace - bool ValidateGenerateOptions(const GenerateOptions& options, std::string* diagnostic) noexcept { try { diff --git a/src/engine/text_generation/common_autoregressive_generator.h b/src/engine/text_generation/common_autoregressive_generator.h index 4880f55a..f5035cfa 100644 --- a/src/engine/text_generation/common_autoregressive_generator.h +++ b/src/engine/text_generation/common_autoregressive_generator.h @@ -35,6 +35,10 @@ class IAutoregressiveDecoder { bool ValidateGenerateOptions(const GenerateOptions& options, std::string* diagnostic = nullptr) noexcept; +// Returns std::string::npos when no stop word occurs in text. +size_t FindFirstStop(const std::string& text, + const std::vector& stop_words) noexcept; + class CommonAutoregressiveGenerator final { public: static int Generate(IAutoregressiveDecoder& decoder, diff --git a/tests/unit/engine/test_llama_cpp_backend.cpp b/tests/unit/engine/test_llama_cpp_backend.cpp index 03866fc5..4a8262dd 100644 --- a/tests/unit/engine/test_llama_cpp_backend.cpp +++ b/tests/unit/engine/test_llama_cpp_backend.cpp @@ -69,6 +69,36 @@ TEST(LlamaCppBackendTest, } } +TEST(LlamaCppBackendTest, StopWordLookupUsesEarliestMatchRegardlessOfOrder) { + EXPECT_EQ(text_generation::FindFirstStop("beforeafter", + {"", "missing", ""}), + 6u); + EXPECT_EQ(text_generation::FindFirstStop("after", {""}), 0u); + EXPECT_EQ(text_generation::FindFirstStop("text", {"missing"}), + std::string::npos); + EXPECT_EQ(text_generation::FindFirstStop("text", {}), std::string::npos); +} + +TEST(LlamaCppBackendTest, GeneratorStopsOnWordSpanningDecodedPieces) { + // The decoder emits "prefix" on every step; "fixpre" spans two steps. + FailingDecoder decoder(-1); + GenerateOptions options; + options.max_tokens = 3; + options.temperature = 0.0f; + options.stop_words = {"missing", "fixpre"}; + std::string output, diagnostic; + ASSERT_EQ(text_generation::CommonAutoregressiveGenerator::Generate( + decoder, "prompt", false, options, 0, &output, &diagnostic), + error::kOk); + EXPECT_EQ(output, "pre"); + + options.stop_words.clear(); + ASSERT_EQ(text_generation::CommonAutoregressiveGenerator::Generate( + decoder, "prompt", false, options, 0, &output, &diagnostic), + error::kOk); + EXPECT_EQ(output, "prefixprefixprefix"); +} + #ifdef HAVE_LLAMACPP class InvalidGguf { public: