llama_sampler_free(sampler);
}
+static void test_reasoning_budget_force_manual() {
+ const std::vector<llama_token> start = {100};
+ const std::vector<llama_token> end = {101};
+ const std::vector<llama_token> forced = {102, 101};
+
+ // if COUNTING, force() succeeds and begins forcing the end sequence from the start
+ {
+ auto * sampler = common_reasoning_budget_init(nullptr, start, end, forced, 5, REASONING_BUDGET_IDLE);
+
+ llama_sampler_accept(sampler, 100); // COUNTING, remaining=5
+ llama_sampler_accept(sampler, 50); // COUNTING, remaining=4
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING);
+
+ GGML_ASSERT(common_reasoning_budget_force(sampler) && "force() should succeed from COUNTING");
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING);
+
+ // forces the configured sequence from force_pos=0, then transitions to DONE
+ GGML_ASSERT(get_forced_token(sampler, 102) == 102);
+ llama_sampler_accept(sampler, 102);
+ GGML_ASSERT(get_forced_token(sampler, 102) == 101);
+ llama_sampler_accept(sampler, 101);
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE);
+
+ llama_sampler_free(sampler);
+ }
+
+ // if IDLE, force() is a no-op
+ {
+ auto * sampler = common_reasoning_budget_init(nullptr, start, end, forced, 5, REASONING_BUDGET_IDLE);
+
+ GGML_ASSERT(!common_reasoning_budget_force(sampler) && "force() must not transition from IDLE");
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_IDLE);
+
+ llama_sampler_free(sampler);
+ }
+
+ // if DONE, force() is a no-op
+ {
+ auto * sampler = common_reasoning_budget_init(nullptr, start, end, forced, 5, REASONING_BUDGET_IDLE);
+
+ llama_sampler_accept(sampler, 100); // COUNTING
+ llama_sampler_accept(sampler, 101); // natural end -> DONE
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE);
+
+ GGML_ASSERT(!common_reasoning_budget_force(sampler) && "force() must not transition from DONE");
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE);
+
+ llama_sampler_free(sampler);
+ }
+
+ // if FORCING, force() is a no-op and must not rewind the force position
+ {
+ auto * sampler = common_reasoning_budget_init(nullptr, start, end, forced, 0, REASONING_BUDGET_FORCING);
+
+ GGML_ASSERT(get_forced_token(sampler, 102) == 102);
+ llama_sampler_accept(sampler, 102); // advance to the second forced token (force_pos=1)
+
+ GGML_ASSERT(!common_reasoning_budget_force(sampler) && "force() must not transition from FORCING");
+ GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING);
+ GGML_ASSERT(get_forced_token(sampler, 102) == 101 && "force() must not rewind the force position");
+
+ llama_sampler_free(sampler);
+ }
+
+ // a null sampler is safely ignored
+ GGML_ASSERT(!common_reasoning_budget_force(nullptr));
+
+ fprintf(stderr, " Test 'manual force transition' passed\n");
+}
+
// UTF-8 boundary detection unit test
// Tests common_utf8_is_complete() from reasoning-budget.h
static void test_utf8_boundary_detection() {
test_reasoning_budget_clone_mid_counting();
test_reasoning_budget_clone_mid_forcing();
+ test_reasoning_budget_force_manual();
- printf("OK (8 tests passed)\n");
+ printf("OK (9 tests passed)\n");
printf("Testing UTF-8 boundary detection... ");
test_utf8_boundary_detection();