/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * Unit tests for openai_chat.c's request PARSE (tool_choice handling + the * forced_name lifetime) and its response SHAPE (where session_conflict lands in * the emitted JSON, on both the streaming and non-streaming paths). * * ── Request parse: tool_choice + forced_name LIFETIME (n2 tool-calling * Phase 5 — BUG 2 regression). * * BUG 2 was an intermittent request failure introduced with the new tool_choice * parsing. The forced-function name (from the object form * {"type":"function","function":{"name":"X"}}) was kept as a BORROWED pointer * into the request's jr_value DOM — memory the caller frees with jr_free(root). * Any change to the free ordering, or a path that holds gen.forced_name past the * DOM, turns that alias into a use-after-free. The fix OWNS the name on the * request (strdup, freed in chat_request_free), so it never aliases freed DOM. * * These tests parse a request, FREE the DOM, then still read gen.forced_name the * way the generator does — proving the kept string is request-owned and stable. * They also cover the string forms (auto/none/required) and the tools gate. * * ── Response shape: session_conflict. See the block above those tests. * * parse_chat_request, send_completion_json and send_usage_chunk are all static, * so the .c is #included directly (the engine-side handlers it also defines are * satisfied by tiny link stubs below — neither path calls them; the two hs_send_* * stubs capture the emitted bytes). Pure C, libc-only; no CUDA, no model. */ #include "lib/test.h" #include /* dup/dup2/read/close — the stderr capture below */ /* Pull in the unit under test (statics reachable). Rename its main out. */ #define main oai_chat_main_unused #include "../src/serve/openai_chat.c" #undef main #include "../src/serve/serve_emitter.h" /* ── Link stubs — the request-PARSE path never invokes these; they only satisfy * the linker for the handler functions compiled into this TU. ── */ void bs_notify_pressure(pn_batch_sched *a, int b) { (void)a; (void)b; } bs_acquire_result bs_slot_acquire_or_wait(pn_batch_sched *a, int b, int *c, int *d, const char *e) { (void)a; (void)b; (void)c; (void)d; (void)e; return BS_ACQUIRE_OK; } void hs_begin_sse(int a) { (void)a; } int hs_client_alive(int a) { (void)a; return 1; } void hs_end_sse(int a) { (void)a; } void hs_send_error(int a, int b, const char *c, const char *d) { (void)a; (void)b; (void)c; (void)d; } void hs_send_error_ex(int a, int b, const char *c, const char *d, const char *e) { (void)a; (void)b; (void)c; (void)d; (void)e; } /* ── Wire capture — the two sinks openai_chat.c serializes a response into. * The response-shape tests at the bottom read back the exact bytes a client * would receive, so a field that stops arriving fails the build. ── */ static char wire_json[8192]; /* last hs_send_json body (non-streaming) */ static char wire_sse[8192]; /* last hs_send_sse event (streaming chunk) */ void hs_send_json(int a, int b, const char *c) { (void)a; (void)b; snprintf(wire_json, sizeof(wire_json), "%s", c ? c : ""); } /* EVERY streamed byte, in order, as a client receives it (events and SSE * comments) — the tool-call streaming tests fold this the way a client does. */ static json_writer wire_all; void hs_send_sse(int a, const char *b) { (void)a; snprintf(wire_sse, sizeof(wire_sse), "%s", b ? b : ""); if (wire_all.data) { jw_raw(&wire_all, "data: "); jw_raw(&wire_all, b ? b : ""); jw_raw(&wire_all, "\n\n"); } } void hs_send_sse_comment(int a, const char *b) { (void)a; if (wire_all.data) { jw_raw(&wire_all, ": "); jw_raw(&wire_all, b); jw_raw(&wire_all, "\n\n"); } } mr_entry *mr_acquire(mr_registry *a, const char *b) { (void)a; (void)b; return NULL; } int mr_count(mr_registry *a) { (void)a; return 0; } void mr_release(mr_registry *a, mr_entry *b) { (void)a; (void)b; } int mr_snapshot(mr_registry *a, mr_info *b, int c) { (void)a; (void)b; (void)c; return 0; } bool mr_model_is_minicpm(const pn_model *a) { (void)a; return false; } void pn_memwatch_read(pn_mem_state *a) { (void)a; } bool pn_memwatch_running(void) { return false; } /* Set by a test to say whether the stubbed generation ENFORCED response_format, * so the streaming path's status resolution can be driven both ways. */ static int stub_response_format_ok = 0; /* Tokens the stubbed generation emits through a REAL emitter (wired exactly as * serve_generation.c wires it, tool tap included), so run_streaming is driven * end to end. NULL = emit nothing (the response-shape tests). */ static const char *const *stub_pieces = NULL; static int stub_n_pieces = 0; /* wire_all.len when the LAST piece had been fed — i.e. what the client had * received while the model was still generating. */ static size_t stub_wire_len_before_end = 0; int sg_run_completion(pn_model *a, pn_state *b, const sg_message *c, int d, const sg_gen_params *e, sg_delta_fn f, void *g, sg_result *h, const sg_prefix_ctx *i) { (void)a; (void)b; (void)c; (void)d; (void)i; if (h) h->response_format_ok = stub_response_format_ok; if (!stub_pieces) return 0; emitter em; em_init(&em, f, g, e->stop, e->n_stop, e->has_tools, e->dialect); em_set_tool_tap(&em, e->tool_tap, e->tool_tap_ud); em.in_think = 0; for (int k = 0; k < stub_n_pieces; k++) em_feed(&em, stub_pieces[k]); stub_wire_len_before_end = wire_all.len; em_flush(&em); h->content = em.content.data ? em.content.data : strdup(""); h->tool_raw = em.tool.data; h->finish_reason = "stop"; em.content.data = NULL; em.tool.data = NULL; em_free(&em); return 0; } int sg_run_completion_batched(pn_model *a, pn_batch_sched *b, int c, const sg_message *d, int e, const sg_gen_params *f, sg_delta_fn g, void *h, sg_result *i, const char *j) { (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g; (void)h; (void)i; (void)j; return 0; } void sg_result_free(sg_result *a) { (void)a; } bool sg_tool_choice_required_with_thinking(tc_dialect d) { (void)d; return false; } /* Phase-7 warm-tier externals the session handlers reference (never called on * the request-parse path this test exercises). */ double sg_prefill_tok_per_s_measured(void) { return 0.0; } uint64_t mr_weights_version(const mr_entry *a) { (void)a; return 0; } unsigned char *pn_base64_decode(const char *a, size_t *b) { (void)a; if (b) *b = 0; return NULL; } char *pn_base64_encode(const unsigned char *a, size_t b) { (void)a; (void)b; return NULL; } void *pn_qwen35moe_snap_deserialize(const unsigned char *a, size_t b, uint64_t *c) { (void)a; (void)b; if (c) *c = 0; return NULL; } void pn_qwen35moe_state_snapshot_free(void *a) { (void)a; } pn_session_admit pn_prefix_cache_admit(pn_prefix_cache *a, const char *b, uint64_t c, uint64_t d, int *e) { (void)a; (void)b; (void)c; (void)d; if (e) *e = 0; return PN_SESSION_ADMIT_OK; } void pn_prefix_cache_touch(pn_prefix_cache *a, const char *b, uint64_t c) { (void)a; (void)b; (void)c; } int pn_prefix_cache_import_snapshot(pn_prefix_cache *a, const char *b, const void *c, const uint32_t *d, int e) { (void)a; (void)b; (void)c; (void)d; (void)e; return 0; } /* Bodies exercised. */ static const char *BODY_FUNCTION = "{\"model\":\"nex\",\"messages\":[{\"role\":\"user\",\"content\":\"read it\"}]," "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"," "\"parameters\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}}}]," "\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"read\"}}}"; static const char *BODY_REQUIRED = "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]," "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]," "\"tool_choice\":\"required\"}"; /* ============================================================ * BUG 2: the forced_name must survive the DOM being freed. We parse, free the * jr_value tree, then read gen.forced_name the way the generator does. With the * borrowed-pointer regression this is a use-after-free; with the owned copy it * is stable ("read"). Looping stresses the allocator so a dangling read would * reliably read reused/poisoned bytes. * ============================================================ */ static void test_forced_name_survives_dom_free(void) { for (int i = 0; i < 256; i++) { jr_value *root = jr_parse(BODY_FUNCTION); ASSERT_NOT_NULL(root); chat_request req; const char *err = NULL; ASSERT_TRUE(parse_chat_request(root, &req, &err) == 0); ASSERT_TRUE(req.gen.tool_choice == SG_TOOL_CHOICE_FUNCTION); ASSERT_NOT_NULL(req.gen.forced_name); ASSERT_TRUE(strcmp(req.gen.forced_name, "read") == 0); /* Free the DOM FIRST — exactly what oai_handle_chat does after the * completion. forced_name must NOT point into it. */ jr_free(root); /* Now read the kept name (generator does this via sg_tg_setup). */ ASSERT_TRUE(strlen(req.gen.forced_name) == 4); ASSERT_TRUE(strcmp(req.gen.forced_name, "read") == 0); chat_request_free(&req); } } /* tool_choice string forms resolve correctly and carry no forced_name. */ /* The prefix-cache session key: `session_id`, then OpenAI's `prompt_cache_key`, then * opencode's `promptCacheKey`; empty strings don't count. Without one the serve caches * nothing, so a client using either alias would re-prefill every turn. */ static void test_session_key_aliases(void) { struct { const char *fields; const char *want; } cases[] = { { "\"session_id\":\"a\"", "a" }, { "\"prompt_cache_key\":\"b\"", "b" }, { "\"promptCacheKey\":\"c\"", "c" }, { "\"promptCacheKey\":\"c\",\"session_id\":\"a\"", "a" }, { "\"promptCacheKey\":\"c\",\"prompt_cache_key\":\"b\"", "b" }, { "\"session_id\":\"\",\"promptCacheKey\":\"c\"", "c" }, { "\"user\":\"u\"", NULL }, }; for (unsigned k = 0; k < sizeof(cases) / sizeof(cases[0]); k++) { char body[512]; snprintf(body, sizeof(body), "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}],%s}", cases[k].fields); jr_value *root = jr_parse(body); ASSERT_NOT_NULL(root); chat_request req; const char *err = NULL; ASSERT_TRUE(parse_chat_request(root, &req, &err) == 0); if (cases[k].want) { ASSERT_NOT_NULL(req.session_id); ASSERT_TRUE(strcmp(req.session_id, cases[k].want) == 0); } else { ASSERT_TRUE(req.session_id == NULL); } chat_request_free(&req); jr_free(root); } } static void test_tool_choice_string_forms(void) { struct { const char *tc; sg_tool_choice_kind want; bool tools; } cases[] = { { "\"required\"", SG_TOOL_CHOICE_REQUIRED, true }, { "\"auto\"", SG_TOOL_CHOICE_AUTO, true }, { "\"none\"", SG_TOOL_CHOICE_AUTO, false }, /* none → tools dropped → AUTO */ { "\"bogus\"", SG_TOOL_CHOICE_AUTO, true }, /* unknown → auto */ }; for (unsigned k = 0; k < sizeof(cases) / sizeof(cases[0]); k++) { char body[512]; snprintf(body, sizeof(body), "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]," "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]," "\"tool_choice\":%s}", cases[k].tc); jr_value *root = jr_parse(body); ASSERT_NOT_NULL(root); chat_request req; const char *err = NULL; ASSERT_TRUE(parse_chat_request(root, &req, &err) == 0); ASSERT_TRUE(req.gen.tool_choice == cases[k].want); ASSERT_TRUE((req.tools != NULL) == cases[k].tools); ASSERT_NULL(req.gen.forced_name); chat_request_free(&req); jr_free(root); } } /* "required" with tools present is honored; forced_name stays NULL. */ static void test_required_no_forced_name(void) { jr_value *root = jr_parse(BODY_REQUIRED); ASSERT_NOT_NULL(root); chat_request req; const char *err = NULL; ASSERT_TRUE(parse_chat_request(root, &req, &err) == 0); ASSERT_TRUE(req.gen.tool_choice == SG_TOOL_CHOICE_REQUIRED); ASSERT_NULL(req.gen.forced_name); chat_request_free(&req); jr_free(root); } /* No tools present → forcing is meaningless: tool_choice downgrades to AUTO even * when the request asked for a specific function. */ static void test_forced_without_tools_downgrades(void) { const char *body = "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]," "\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"read\"}}}"; jr_value *root = jr_parse(body); ASSERT_NOT_NULL(root); chat_request req; const char *err = NULL; ASSERT_TRUE(parse_chat_request(root, &req, &err) == 0); ASSERT_NULL(req.tools); ASSERT_TRUE(req.gen.tool_choice == SG_TOOL_CHOICE_AUTO); ASSERT_NULL(req.gen.forced_name); chat_request_free(&req); jr_free(root); } /* Malformed object form (missing name) does not force and does not crash. */ static void test_malformed_object_form(void) { const char *bodies[] = { "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]," "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]," "\"tool_choice\":{}}", "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]," "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]," "\"tool_choice\":{\"type\":\"function\"}}", "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]," "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]," "\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"\"}}}", }; for (unsigned k = 0; k < sizeof(bodies) / sizeof(bodies[0]); k++) { jr_value *root = jr_parse(bodies[k]); ASSERT_NOT_NULL(root); chat_request req; const char *err = NULL; ASSERT_TRUE(parse_chat_request(root, &req, &err) == 0); ASSERT_TRUE(req.gen.tool_choice == SG_TOOL_CHOICE_AUTO); /* not FUNCTION */ ASSERT_NULL(req.gen.forced_name); chat_request_free(&req); jr_free(root); } } /* ============================================================ * RESPONSE SHAPE — session_conflict on the wire. * * The serve detects a second conversation wearing one session id and reports it * with "session_conflict":true INSIDE the `usage` object — the object a client * already reads for kv_used/kv_cap (an agent client's SSE parser reads the * terminal usage chunk). It shipped emitted OUTSIDE `usage` on the * non-streaming path (the usage object is closed by the jw_printf format, so an * appended field lands as a sibling of `usage`, not a member), which is why no * client ever saw it. These tests parse the emitted bytes and assert the field * is a MEMBER of `usage` on BOTH paths — a substring check would have passed * through the whole bug. * ============================================================ */ /* A minimal successful result, as the generator fills it. */ static sg_result wire_test_result(int session_conflict) { static char content[] = "hi"; sg_result res = {0}; res.content = content; res.finish_reason = "stop"; res.prompt_tokens = 33; res.completion_tokens = 4; res.kv_used_tokens = 33; res.kv_cap_tokens = 32000; res.session_conflict = session_conflict; return res; } /* Non-streaming: the flag rides `usage`, not the enclosing completion object. */ static void test_nonstream_usage_carries_session_conflict(void) { sg_result res = wire_test_result(1); wire_json[0] = '\0'; chat_choice ch = { .res = res }; send_completion_json(-1, "cmpl-1", 0, "test-model", &ch, 1, NULL, NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); const jr_value *usage = jr_object_get(root, "usage"); ASSERT_NOT_NULL(usage); ASSERT_TRUE(jr_as_bool(jr_object_get(usage, "session_conflict"), false)); /* Not a top-level sibling: a client reading `usage` must find it. */ ASSERT_NULL(jr_object_get(root, "session_conflict")); /* The fields it rides beside are untouched. */ ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "kv_used"), -1), 33); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "kv_cap"), -1), 32000); jr_free(root); } /* No substitution detected → the field is absent everywhere (never a latched or * always-present "false" a client could misread as proof of correctness). */ static void test_nonstream_usage_omits_session_conflict_when_clean(void) { sg_result res = wire_test_result(0); wire_json[0] = '\0'; chat_choice ch = { .res = res }; send_completion_json(-1, "cmpl-2", 0, "test-model", &ch, 1, NULL, NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); const jr_value *usage = jr_object_get(root, "usage"); ASSERT_NOT_NULL(usage); ASSERT_NULL(jr_object_get(usage, "session_conflict")); ASSERT_NULL(jr_object_get(root, "session_conflict")); jr_free(root); } /* Streaming: the terminal usage chunk (stream_options.include_usage) carries it * in the same place — this is the path the client actually uses. */ static void test_stream_usage_chunk_carries_session_conflict(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-3", .created = 0, .model_id = "test-model", .role_sent = true }; wire_sse[0] = '\0'; send_usage_chunk(&sc, 33, 4, 0, 33, 32000, 1); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); const jr_value *usage = jr_object_get(root, "usage"); ASSERT_NOT_NULL(usage); ASSERT_TRUE(jr_as_bool(jr_object_get(usage, "session_conflict"), false)); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "kv_used"), -1), 33); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "total_tokens"), -1), 37); jr_free(root); } static void test_stream_usage_chunk_omits_session_conflict_when_clean(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-4", .created = 0, .model_id = "test-model", .role_sent = true }; wire_sse[0] = '\0'; send_usage_chunk(&sc, 33, 4, 0, 33, 32000, 0); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); const jr_value *usage = jr_object_get(root, "usage"); ASSERT_NOT_NULL(usage); ASSERT_NULL(jr_object_get(usage, "session_conflict")); jr_free(root); } /* ============================================================ * RESPONSE SHAPE — prompt_tokens_details.cached_tokens on the wire. * * The serve knew how many prompt tokens it reused (the prefix cache logs it to * stderr) and told the client nothing, so a client's prefill rate was * prompt_tokens / TTFT — a number that RISES the more the serve caches (measured * 409 tok/s against a true 39.7 at 150 of 166 reused). These tests read the * emitted bytes back and assert the OpenAI-standard nesting on BOTH paths: a * MEMBER named cached_tokens inside a `prompt_tokens_details` object inside * `usage`. They also pin the two properties a client depends on — the field is * present on a COLD turn carrying 0 (absent and zero are different answers), and * cached_tokens never exceeds prompt_tokens. * ============================================================ */ /* Fetch usage.prompt_tokens_details.cached_tokens out of an emitted body, or * -1 when either object is missing. */ static int wire_cached_tokens(const jr_value *root) { const jr_value *usage = jr_object_get(root, "usage"); if (!usage) return -1; const jr_value *details = jr_object_get(usage, "prompt_tokens_details"); if (!details) return -1; const jr_value *cached = jr_object_get(details, "cached_tokens"); if (!cached) return -1; return (int)jr_as_number(cached, -1); } /* Warm turn, non-streaming: the reused count reaches the client, nested where * an OpenAI client already looks for it. */ static void test_nonstream_usage_reports_cached_tokens(void) { sg_result res = wire_test_result(0); res.prompt_tokens = 166; res.cached_prompt_tokens = 150; wire_json[0] = '\0'; chat_choice ch = { .res = res }; send_completion_json(-1, "cmpl-5", 0, "test-model", &ch, 1, NULL, NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(wire_cached_tokens(root), 150); /* The invariant a rate calculation divides by: computed = prompt - cached * must stay positive, so cached can never exceed prompt. */ const jr_value *usage = jr_object_get(root, "usage"); int prompt = (int)jr_as_number(jr_object_get(usage, "prompt_tokens"), -1); ASSERT_INT_EQ(prompt, 166); ASSERT_TRUE(wire_cached_tokens(root) <= prompt); /* Not a top-level sibling, and not flattened beside prompt_tokens. */ ASSERT_NULL(jr_object_get(root, "prompt_tokens_details")); ASSERT_NULL(jr_object_get(usage, "cached_tokens")); jr_free(root); } /* Cold turn, non-streaming: PRESENT and 0. If this ever goes back to being * omitted, a client cannot distinguish "nothing was reused" from "this serve * does not report reuse" and has to fall back to the inflated rate. */ static void test_nonstream_usage_reports_zero_cached_tokens_when_cold(void) { sg_result res = wire_test_result(0); res.cached_prompt_tokens = 0; wire_json[0] = '\0'; chat_choice ch = { .res = res }; send_completion_json(-1, "cmpl-6", 0, "test-model", &ch, 1, NULL, NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(wire_cached_tokens(root), 0); jr_free(root); } /* Warm turn, streaming terminal usage chunk — the path benchmarks measure. */ static void test_stream_usage_chunk_reports_cached_tokens(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-7", .created = 0, .model_id = "test-model", .role_sent = true }; wire_sse[0] = '\0'; send_usage_chunk(&sc, 166, 4, 150, 170, 32000, 0); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(wire_cached_tokens(root), 150); const jr_value *usage = jr_object_get(root, "usage"); ASSERT_TRUE(wire_cached_tokens(root) <= (int)jr_as_number(jr_object_get(usage, "prompt_tokens"), -1)); /* The fields it rides beside are untouched. */ ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "total_tokens"), -1), 170); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "kv_used"), -1), 170); jr_free(root); } /* Cold turn, streaming: same present-and-zero rule as the non-streaming body. */ static void test_stream_usage_chunk_reports_zero_cached_tokens_when_cold(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-8", .created = 0, .model_id = "test-model", .role_sent = true }; wire_sse[0] = '\0'; send_usage_chunk(&sc, 166, 4, 0, 170, 32000, 0); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(wire_cached_tokens(root), 0); jr_free(root); } /* ============================================================ * OpenAI FIELDS THAT USED TO BE SILENTLY IGNORED * * response_format / seed / logit_bias / presence_penalty / frequency_penalty / * logprobs / top_logprobs all parsed to nothing before this change: a client * sending response_format:{"type":"json_object"} believed it was guaranteed JSON * and quietly was not. These tests assert the parse RESULT (what the generator * actually receives) and, for response_format, the reported status on BOTH the * streaming and non-streaming wire — parsed back out of the emitted bytes, not * substring-matched. * ============================================================ */ /* Parse a body and hand back the request; caller frees both. */ static jr_value *parse_body(const char *body, chat_request *req) { jr_value *root = jr_parse(body); if (!root) return NULL; const char *err = NULL; if (parse_chat_request(root, req, &err) != 0) { jr_free(root); return NULL; } return root; } #define BODY_HEAD "{\"messages\":[{\"role\":\"user\",\"content\":\"x\"}]" static void test_response_format_json_object_is_applied(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_JSON_OBJECT); ASSERT_NOT_NULL(req.response_format_status); ASSERT_STR_EQ(req.response_format_status, "applied"); chat_request_free(&req); jr_free(root); } static void test_response_format_text_is_a_noop(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"text\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_TEXT); /* The explicit default constrains nothing and needs no report. */ ASSERT_NULL(req.response_format_status); chat_request_free(&req); jr_free(root); } static void test_response_format_absent_is_a_noop(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD "}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_TEXT); ASSERT_NULL(req.response_format_status); chat_request_free(&req); jr_free(root); } static void test_response_format_json_schema_downgrades(void) { /* The shape is not enforced, but the reply is still constrained to valid * JSON and the client is TOLD the schema did not apply. */ chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"json_schema\",\"json_schema\":" "{\"name\":\"r\",\"schema\":{\"type\":\"object\"}}}}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_JSON_OBJECT); ASSERT_STR_EQ(req.response_format_status, "schema_downgraded"); chat_request_free(&req); jr_free(root); } static void test_response_format_unknown_type_is_reported(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"xml\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_TEXT); ASSERT_STR_EQ(req.response_format_status, "ignored_unknown_type"); chat_request_free(&req); jr_free(root); } /* THE PRECEDENCE RULE. Tools and response_format are two masks over one decode * and the sampler has one hook. Tools win — and the client is told, so the * conflict is visible rather than silent. */ static void test_response_format_yields_to_tools(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]" ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_NOT_NULL(req.tools); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_TEXT); ASSERT_STR_EQ(req.response_format_status, "ignored_tools_present"); chat_request_free(&req); jr_free(root); } /* tool_choice:"none" NULLs the tools, so nothing is competing for the mask and * the JSON constraint applies after all. */ static void test_response_format_applies_when_tool_choice_none(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]" ",\"tool_choice\":\"none\"" ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_NULL(req.tools); ASSERT_TRUE(req.gen.response_format == SG_RESPONSE_FORMAT_JSON_OBJECT); ASSERT_STR_EQ(req.response_format_status, "applied"); chat_request_free(&req); jr_free(root); } static void test_seed_is_parsed(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD ",\"seed\":12345}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.seed_set); ASSERT_TRUE(req.gen.seed == 12345u); chat_request_free(&req); jr_free(root); /* Absent → unset, so the global RNG is used exactly as before. */ root = parse_body(BODY_HEAD "}", &req); ASSERT_NOT_NULL(root); ASSERT_FALSE(req.gen.seed_set); chat_request_free(&req); jr_free(root); /* seed 0 is a REAL seed, not "absent". */ root = parse_body(BODY_HEAD ",\"seed\":0}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.seed_set); ASSERT_TRUE(req.gen.seed == 0u); chat_request_free(&req); jr_free(root); } /* `enable_thinking` is the one reasoning control the Qwen3.x template exposes, * and it is BINARY. Absent must stay DEFAULT — that is what keeps every * existing client byte-identical, and it is the half a "the field parses" test * would miss. */ static void test_enable_thinking_is_parsed(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD ",\"enable_thinking\":false}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.thinking == SG_THINKING_OFF); chat_request_free(&req); jr_free(root); root = parse_body(BODY_HEAD ",\"enable_thinking\":true}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.thinking == SG_THINKING_ON); chat_request_free(&req); jr_free(root); /* Absent → DEFAULT: the env still decides, exactly as before. */ root = parse_body(BODY_HEAD "}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.thinking == SG_THINKING_DEFAULT); chat_request_free(&req); jr_free(root); /* A non-boolean is NOT a silent "false" — it leaves the default standing * rather than quietly switching a client's reasoning off. */ root = parse_body(BODY_HEAD ",\"enable_thinking\":\"low\"}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.thinking == SG_THINKING_DEFAULT); chat_request_free(&req); jr_free(root); } static void test_penalties_are_parsed_and_clamped(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"presence_penalty\":1.5,\"frequency_penalty\":-0.25}", &req); ASSERT_NOT_NULL(root); ASSERT_FLT_NEAR(req.gen.presence_penalty, 1.5f, 1e-6f); ASSERT_FLT_NEAR(req.gen.frequency_penalty, -0.25f, 1e-6f); chat_request_free(&req); jr_free(root); /* Out of OpenAI's documented range → clamped, never passed through raw. */ root = parse_body( BODY_HEAD ",\"presence_penalty\":99,\"frequency_penalty\":-99}", &req); ASSERT_NOT_NULL(root); ASSERT_FLT_NEAR(req.gen.presence_penalty, 2.0f, 1e-6f); ASSERT_FLT_NEAR(req.gen.frequency_penalty, -2.0f, 1e-6f); chat_request_free(&req); jr_free(root); } static void test_logit_bias_is_parsed_and_owned(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"logit_bias\":{\"100\":-100,\"7\":2.5,\"bogus\":1,\"9\":\"x\"}}", &req); ASSERT_NOT_NULL(root); /* Non-numeric key and non-numeric value are skipped, not fatal. */ ASSERT_INT_EQ(req.gen.n_logit_bias, 2); ASSERT_NOT_NULL(req.gen.logit_bias); /* The map must survive the DOM being freed — the same lifetime rule * forced_name needed (BUG 2); the generator reads it after jr_free(root). */ jr_free(root); ASSERT_TRUE(req.gen.logit_bias[0].token_id == 100u); ASSERT_FLT_NEAR(req.gen.logit_bias[0].bias, -100.0f, 1e-6f); ASSERT_TRUE(req.gen.logit_bias[1].token_id == 7u); ASSERT_FLT_NEAR(req.gen.logit_bias[1].bias, 2.5f, 1e-6f); chat_request_free(&req); } static void test_logprobs_are_parsed(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"logprobs\":true,\"top_logprobs\":5}", &req); ASSERT_NOT_NULL(root); ASSERT_TRUE(req.gen.logprobs); ASSERT_INT_EQ(req.gen.logprobs_top, 5); chat_request_free(&req); jr_free(root); /* Over the cap → clamped (the sampler writes into fixed-size arrays). */ root = parse_body(BODY_HEAD ",\"logprobs\":true,\"top_logprobs\":500}", &req); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(req.gen.logprobs_top, PN_LOGPROBS_MAX_TOP); chat_request_free(&req); jr_free(root); /* Absent → off, sampler untouched. */ root = parse_body(BODY_HEAD "}", &req); ASSERT_NOT_NULL(root); ASSERT_FALSE(req.gen.logprobs); chat_request_free(&req); jr_free(root); } /* ── response_format_status ON THE WIRE, both paths ── * The last bug in this file was a field that landed correctly on one path and * not the other, so both are asserted by PARSING the emitted bytes. */ static void test_nonstream_reports_response_format_status(void) { sg_result res = wire_test_result(0); wire_json[0] = '\0'; chat_choice ch = { .res = res }; send_completion_json(-1, "cmpl-rf", 0, "test-model", &ch, 1, "applied", NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); const jr_value *status = jr_object_get(root, "response_format_status"); ASSERT_NOT_NULL(status); ASSERT_STR_EQ(jr_as_string(status), "applied"); /* It must not have disturbed the object it sits beside. */ ASSERT_NOT_NULL(jr_object_get(root, "usage")); ASSERT_NOT_NULL(jr_object_get(root, "choices")); jr_free(root); } static void test_nonstream_omits_status_when_not_requested(void) { sg_result res = wire_test_result(0); wire_json[0] = '\0'; chat_choice ch = { .res = res }; send_completion_json(-1, "cmpl-rf2", 0, "test-model", &ch, 1, NULL, NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); ASSERT_NULL(jr_object_get(root, "response_format_status")); jr_free(root); } static void test_stream_chunk_reports_response_format_status(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-rf3", .created = 0, .model_id = "test-model", .role_sent = true, .response_format_status = "ignored_tools_present" }; wire_sse[0] = '\0'; send_chunk(&sc, "", "stop", NULL, 0); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); const jr_value *status = jr_object_get(root, "response_format_status"); ASSERT_NOT_NULL(status); ASSERT_STR_EQ(jr_as_string(status), "ignored_tools_present"); /* Still a well-formed chunk: choices[0].finish_reason survives. */ const jr_value *choice = jr_array_get(jr_object_get(root, "choices"), 0); ASSERT_NOT_NULL(choice); ASSERT_STR_EQ(jr_as_string(jr_object_get(choice, "finish_reason")), "stop"); jr_free(root); } static void test_stream_chunk_omits_status_when_not_requested(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-rf4", .created = 0, .model_id = "test-model", .role_sent = true }; wire_sse[0] = '\0'; send_chunk(&sc, "", "stop", NULL, 0); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); ASSERT_NULL(jr_object_get(root, "response_format_status")); jr_free(root); } /* ── OpenAI `n` (multiple completions) ── */ static void test_n_defaults_to_one(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD "}", &req); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(req.n_choices, 1); ASSERT_NULL(req.n_status); chat_request_free(&req); jr_free(root); } static void test_n_is_honoured(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD ",\"n\":3}", &req); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(req.n_choices, 3); ASSERT_NULL(req.n_status); chat_request_free(&req); jr_free(root); } static void test_n_over_the_ceiling_is_reported_not_silent(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD ",\"n\":500}", &req); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(req.n_choices, OAI_MAX_CHOICES); /* The clamp must be VISIBLE — a silent cap is the defect class this whole * change exists to remove. */ ASSERT_NOT_NULL(req.n_status); ASSERT_STR_EQ(req.n_status, "capped_to_8"); chat_request_free(&req); jr_free(root); } static void test_n_zero_and_negative_floor_at_one(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD ",\"n\":0}", &req); ASSERT_NOT_NULL(req.n_choices == 1 ? root : NULL); chat_request_free(&req); jr_free(root); root = parse_body(BODY_HEAD ",\"n\":-4}", &req); ASSERT_NOT_NULL(root); ASSERT_INT_EQ(req.n_choices, 1); chat_request_free(&req); jr_free(root); } /* A seeded n>1 request must produce n DIFFERENT completions — reusing one RNG * stream would make them identical, which is not what `seed` means. */ static void test_seeded_choices_get_distinct_streams(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD ",\"seed\":7,\"n\":3}", &req); ASSERT_NOT_NULL(root); sg_gen_params a = gen_params_for_choice(&req, 0); sg_gen_params b = gen_params_for_choice(&req, 1); sg_gen_params c = gen_params_for_choice(&req, 2); ASSERT_TRUE(a.seed_set && b.seed_set && c.seed_set); ASSERT_TRUE(a.seed != b.seed); ASSERT_TRUE(b.seed != c.seed); /* …and re-deriving choice 0 gives the same stream, so the whole REQUEST is * still reproducible. */ ASSERT_TRUE(gen_params_for_choice(&req, 0).seed == a.seed); chat_request_free(&req); jr_free(root); } /* The wire shape of an n>1 response: n choices with ascending indices, one * prompt charged once, completion tokens summed. */ static void test_nonstream_emits_all_choices(void) { sg_result r0 = wire_test_result(0); sg_result r1 = wire_test_result(0); static char alt[] = "yo"; r1.content = alt; r1.completion_tokens = 6; chat_choice chs[2] = { { .res = r0 }, { .res = r1 } }; wire_json[0] = '\0'; send_completion_json(-1, "cmpl-n", 0, "test-model", chs, 2, NULL, NULL); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); const jr_value *choices = jr_object_get(root, "choices"); ASSERT_INT_EQ(jr_array_len(choices), 2); const jr_value *c0 = jr_array_get(choices, 0); const jr_value *c1 = jr_array_get(choices, 1); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(c0, "index"), -1), 0); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(c1, "index"), -1), 1); ASSERT_STR_EQ(jr_as_string(jr_object_get(jr_object_get(c1, "message"), "content")), "yo"); const jr_value *usage = jr_object_get(root, "usage"); /* The prompt was prefilled once; the completions add up. */ ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "prompt_tokens"), -1), 33); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "completion_tokens"), -1), 4 + 6); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(usage, "total_tokens"), -1), 33 + 10); jr_free(root); } static void test_nonstream_reports_n_shortfall(void) { sg_result r0 = wire_test_result(0); chat_choice ch = { .res = r0 }; wire_json[0] = '\0'; send_completion_json(-1, "cmpl-n2", 0, "test-model", &ch, 1, NULL, "partial_slot_unavailable"); jr_value *root = jr_parse(wire_json); ASSERT_NOT_NULL(root); ASSERT_STR_EQ(jr_as_string(jr_object_get(root, "n_status")), "partial_slot_unavailable"); jr_free(root); } static void test_stream_chunk_carries_the_choice_index(void) { stream_ctx sc = { .client_fd = -1, .id = "cmpl-n3", .created = 0, .model_id = "test-model", .role_sent = true, .choice_index = 2 }; wire_sse[0] = '\0'; send_chunk(&sc, "\"content\":\"x\"", NULL, NULL, 0); jr_value *root = jr_parse(wire_sse); ASSERT_NOT_NULL(root); const jr_value *choice = jr_array_get(jr_object_get(root, "choices"), 0); ASSERT_INT_EQ((int)jr_as_number(jr_object_get(choice, "index"), -1), 2); jr_free(root); } /* ── the STREAMING path really sets response_format_status ── * The wire-shape tests above set the field on the ctx by hand, which proves the * writer works but NOT that the streaming run path populates it. That gap is * exactly how the previous bug shipped (correct on one path, absent on the * other), so drive run_streaming end-to-end against the stubbed generator and * read the closing chunk back. */ /* Drive run_streaming end-to-end against the stubbed generator and read the * closing chunk back. Asserts BOTH directions: the streaming path must report * "applied" only when the generation actually enforced the constraint, and * "not_enforced" when it did not. A status that claims a guarantee it did not * deliver is the defect this whole change exists to remove. */ static void run_streaming_status_case(int enforced, const char *expect) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_STR_EQ(req.response_format_status, "applied"); /* the INTENT */ stub_response_format_ok = enforced; stream_ctx sc = { .client_fd = -1, .id = "cmpl-s", .created = 0, .model_id = "test-model" }; wire_sse[0] = '\0'; run_streaming(-1, NULL, NULL, &req, &req.gen, &sc, NULL); /* wire_sse holds the LAST chunk written — the closing one. */ jr_value *chunk = jr_parse(wire_sse); ASSERT_NOT_NULL(chunk); ASSERT_STR_EQ(jr_as_string(jr_object_get(chunk, "response_format_status")), expect); jr_free(chunk); chat_request_free(&req); jr_free(root); } static void test_run_streaming_reports_enforced_status(void) { run_streaming_status_case(1, "applied"); } static void test_run_streaming_reports_unenforced_status(void) { run_streaming_status_case(0, "not_enforced"); } static void test_status_applied_requires_actual_enforcement(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_STR_EQ(req.response_format_status, "applied"); /* the INTENT */ sg_result enforced = {0}; enforced.response_format_ok = 1; ASSERT_STR_EQ(wire_response_format_status(&req, &enforced), "applied"); /* The grammar did not close an object → the reply is not guaranteed JSON, * so the wire must NOT claim it was. */ sg_result not_enforced = {0}; not_enforced.response_format_ok = 0; ASSERT_STR_EQ(wire_response_format_status(&req, ¬_enforced), "not_enforced"); chat_request_free(&req); jr_free(root); } static void test_status_schema_downgrade_also_requires_enforcement(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"json_schema\",\"json_schema\":" "{\"name\":\"r\",\"schema\":{\"type\":\"object\"}}}}", &req); ASSERT_NOT_NULL(root); sg_result ok = {0}; ok.response_format_ok = 1; sg_result bad = {0}; bad.response_format_ok = 0; ASSERT_STR_EQ(wire_response_format_status(&req, &ok), "schema_downgraded"); ASSERT_STR_EQ(wire_response_format_status(&req, &bad), "not_enforced"); chat_request_free(&req); jr_free(root); } /* The already-honest dispositions describe the REQUEST, not the generation, so * they must survive untouched (there was nothing to enforce). */ static void test_status_ignored_reasons_pass_through(void) { chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"read\"}}]" ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); sg_result res = {0}; /* response_format_ok 0 — nothing was constrained */ ASSERT_STR_EQ(wire_response_format_status(&req, &res), "ignored_tools_present"); chat_request_free(&req); jr_free(root); root = parse_body(BODY_HEAD ",\"response_format\":{\"type\":\"xml\"}}", &req); ASSERT_NOT_NULL(root); ASSERT_STR_EQ(wire_response_format_status(&req, &res), "ignored_unknown_type"); chat_request_free(&req); jr_free(root); } static void test_status_absent_stays_absent(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD "}", &req); ASSERT_NOT_NULL(root); sg_result res = {0}; ASSERT_NULL(wire_response_format_status(&req, &res)); chat_request_free(&req); jr_free(root); } /* The wire writer must carry the RESOLVED status, so a non-enforcing turn is * visibly non-enforcing in the body a client parses. */ static void test_nonstream_body_carries_not_enforced(void) { sg_result res = wire_test_result(0); res.response_format_ok = 0; chat_choice ch = { .res = res }; chat_request req; jr_value *root = parse_body( BODY_HEAD ",\"response_format\":{\"type\":\"json_object\"}}", &req); ASSERT_NOT_NULL(root); wire_json[0] = '\0'; send_completion_json(-1, "cmpl-ne", 0, "test-model", &ch, 1, wire_response_format_status(&req, &ch.res), NULL); jr_value *body = jr_parse(wire_json); ASSERT_NOT_NULL(body); ASSERT_STR_EQ(jr_as_string(jr_object_get(body, "response_format_status")), "not_enforced"); jr_free(body); chat_request_free(&req); jr_free(root); } /* ============================================================ * RESPONSE SHAPE — tool_call_error on the wire. * * Three conditions the client used to be able to confuse, asserted through the * bytes it actually parses: * 1. ABANDONED — the model opened and stopped: * no `tool_calls`, a `tool_call_error` naming `path`. * 2. EMPTY ARG — the model closed an empty value: a real `tool_calls` * entry with `{"path":""}` arguments, and NO * `tool_call_error`. * 3. NO CALL — neither field. * Conditions 1 and 2 were byte-identical before this (both `read {}`), and the * `{}` half of that collision is why 2 now carries the empty value instead of * dropping it: a dropped member and a member that never arrived read the same. * ============================================================ */ static const char *WIRE_READ_TOOLS = "[{\"type\":\"function\",\"function\":{\"name\":\"read\"," "\"parameters\":{\"type\":\"object\"," "\"properties\":{\"path\":{\"type\":\"string\"}}," "\"required\":[\"path\"]}}}]"; /* Run one turn whose tool channel held `tool_raw` all the way to the emitted * non-streaming body, and return that body parsed. Caller jr_frees it. */ static jr_value *wire_emit_tool_turn_dialect(const char *tool_raw, tc_dialect dialect) { jr_value *tools = jr_parse(WIRE_READ_TOOLS); chat_request req = {0}; req.tools = tools; req.gen.dialect = dialect; chat_choice ch = { .res = wire_test_result(0) }; ch.res.tool_raw = (char *)tool_raw; /* borrowed; not freed through res */ collect_tool_calls(&req, &ch.res, &ch.calls, &ch.n_calls, &ch.tool_error); wire_json[0] = '\0'; send_completion_json(-1, "cmpl-tc", 0, "test-model", &ch, 1, NULL, NULL); tc_tool_calls_free(ch.calls, ch.n_calls); tc_parse_error_free(&ch.tool_error); jr_free(tools); return jr_parse(wire_json); } /* The XML dialect every other wire test uses. */ static jr_value *wire_emit_tool_turn(const char *tool_raw) { return wire_emit_tool_turn_dialect(tool_raw, TC_DIALECT_QWEN_XML); } static const jr_value *wire_first_choice(const jr_value *root) { return jr_array_get(jr_object_get(root, "choices"), 0); } /* 1. ABANDONED — reported, and NOT dressed up as a call. */ static void test_nonstream_body_reports_abandoned_call(void) { jr_value *root = wire_emit_tool_turn( "\n\n\n\n"); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); const jr_value *err = jr_object_get(choice, "tool_call_error"); ASSERT_NOT_NULL(err); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "code")), "call_abandoned"); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "function")), "read"); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "parameter")), "path"); ASSERT_TRUE(jr_as_string(jr_object_get(err, "message")) != NULL); jr_free(root); } /* 2. EMPTY ARGUMENT — still a call, still carrying the argument, and NOT flagged. * * `{"path":""}` on the wire, not `{}`. Empty and absent are different claims and * The client acts differently on each: absent is what a truncated call looks like * and is charged against a budget that ends the task, while empty is a value the * client can judge against the tool (`edit`'s `new_string:""` is a deletion, * `read`'s `path:""` is the empty-path wedge). */ static void test_nonstream_body_keeps_empty_argument_call(void) { jr_value *root = wire_emit_tool_turn( "\n\n\n\n\n" ""); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); const jr_value *tcs = jr_object_get(jr_object_get(choice, "message"), "tool_calls"); ASSERT_NOT_NULL(tcs); ASSERT_INT_EQ(jr_array_len(tcs), 1); ASSERT_STR_EQ(jr_as_string(jr_object_get( jr_object_get(jr_array_get(tcs, 0), "function"), "arguments")), "{\"path\":\"\"}"); ASSERT_NULL(jr_object_get(choice, "tool_call_error")); jr_free(root); } /* 3. NO CALL AT ALL — neither field; the turn is ordinary text. * * The fixture is an EMPTY tool channel, which is what "no call" actually looks * like: the emitter only ever writes into `tool_raw` after it has seen a * tool-call opener, so ordinary prose ends up in `content` and never here. (It * used to pass prose as `tool_raw`, a state the emitter cannot produce; that * shape now means something specific — see the no_call_parsed test below.) */ static void test_nonstream_body_silent_when_no_call(void) { jr_value *root = wire_emit_tool_turn(NULL); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); ASSERT_NULL(jr_object_get(choice, "tool_call_error")); jr_free(root); } /* A tool FRAME was emitted and nothing survived parsing: the emitter diverted a * region (so `tool_raw` is non-empty) but no call came out of it and no more * specific reason was found. Without this the whole diverted region vanishes — * it never reaches `content` either — and the client sees a turn that did * nothing, with no way to know why. qwen-code's analogue is the * MALFORMED_TOOL_CALL raised on a tool_calls finish with zero completed calls * (converter.ts:1699-1711). */ static void test_nonstream_body_reports_unparseable_frame(void) { jr_value *root = wire_emit_tool_turn("\nnothing parseable here\n"); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); const jr_value *err = jr_object_get(choice, "tool_call_error"); ASSERT_NOT_NULL(err); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "code")), "no_call_parsed"); jr_free(root); } /* A truncated JSON argument object reaches the wire as a real tool call rather * than as nothing: the serve closes the one '}' the model never got to write. * This is the whole path — collect_tool_calls asks for the repair, logs it * ([tool_json_repair] on stderr), and the call is serialized normally. */ static void test_nonstream_body_repairs_truncated_json_call(void) { jr_value *root = wire_emit_tool_turn_dialect( "```json\n{\"function\": \"read\", \"arguments\": {\"path\": \"/etc/hostname\"}", TC_DIALECT_ORNITH_JSON); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); const jr_value *tcs = jr_object_get(jr_object_get(choice, "message"), "tool_calls"); ASSERT_NOT_NULL(tcs); ASSERT_INT_EQ(jr_array_len(tcs), 1); ASSERT_STR_EQ(jr_as_string(jr_object_get( jr_object_get(jr_array_get(tcs, 0), "function"), "arguments")), "{\"path\":\"/etc/hostname\"}"); ASSERT_STR_EQ(jr_as_string(jr_object_get(choice, "finish_reason")), "tool_calls"); ASSERT_NULL(jr_object_get(choice, "tool_call_error")); jr_free(root); } /* A call the model named but the request never declared: dropped as before (a * hallucinated tool must never reach the client), but now NAMED, because "you * called a tool that does not exist" is the one drop a client can put back in * front of the model as a correction. */ static void test_nonstream_body_reports_unknown_tool_name(void) { jr_value *root = wire_emit_tool_turn( "\n\n\n/etc\n" "\n\n"); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); const jr_value *err = jr_object_get(choice, "tool_call_error"); ASSERT_NOT_NULL(err); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "code")), "call_name_unknown"); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "function")), "serach_files"); jr_free(root); } /* A healthy call is untouched: the field never appears on a good turn. */ static void test_nonstream_body_clean_call_unflagged(void) { jr_value *root = wire_emit_tool_turn( "\n\n\n/etc/hostname\n" "\n"); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); const jr_value *tcs = jr_object_get(jr_object_get(choice, "message"), "tool_calls"); ASSERT_NOT_NULL(tcs); ASSERT_INT_EQ(jr_array_len(tcs), 1); ASSERT_STR_EQ(jr_as_string(jr_object_get(choice, "finish_reason")), "tool_calls"); ASSERT_NULL(jr_object_get(choice, "tool_call_error")); jr_free(root); } /* ============================================================ * TRUNCATION GATE — a call the DECODING GRAMMAR never closed. * * The one thing the text cannot tell you. A truncated tool call is a legal * PREFIX of a good one, so every fixture below parses cleanly into a named call * with a required argument silently absent — which is exactly what the live * fleet handed to the client (`read {}`, `write` with no `content`) while the serve's * own grammar state said the call had never finished. So the verdict rides in * on sg_result.tool_grammar_verdict, and these tests assert what the client * actually parses. * ============================================================ */ /* Same turn as wire_emit_tool_turn, with the grammar's end-of-turn verdict set. */ static jr_value *wire_emit_tool_turn_verdict(const char *tool_raw, int verdict) { jr_value *tools = jr_parse(WIRE_READ_TOOLS); chat_request req = {0}; req.tools = tools; req.gen.dialect = TC_DIALECT_QWEN_XML; chat_choice ch = { .res = wire_test_result(0) }; ch.res.tool_raw = (char *)tool_raw; /* borrowed; not freed through res */ ch.res.tool_grammar_verdict = verdict; collect_tool_calls(&req, &ch.res, &ch.calls, &ch.n_calls, &ch.tool_error); wire_json[0] = '\0'; send_completion_json(-1, "cmpl-tc", 0, "test-model", &ch, 1, NULL, NULL); tc_tool_calls_free(ch.calls, ch.n_calls); tc_parse_error_free(&ch.tool_error); jr_free(tools); return jr_parse(wire_json); } /* THE DEFECT, verbatim off the live serve log (tool_arg_truncation.md log:274): * the model emitted `path` as a single newline and stopped. It parses into * `read {}` — a named call with its one required argument gone — and used to be * sent as one. It must now be absent from `tool_calls` and NAMED on the error * channel instead. */ static void test_nonstream_body_refuses_incomplete_call(void) { jr_value *root = wire_emit_tool_turn_verdict( "\n\n\n\n" "\n", SG_TOOL_GRAMMAR_INCOMPLETE); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); const jr_value *err = jr_object_get(choice, "tool_call_error"); ASSERT_NOT_NULL(err); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "code")), "call_incomplete"); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "function")), "read"); ASSERT_TRUE(jr_as_string(jr_object_get(err, "message")) != NULL); jr_free(root); } /* The 2-required-argument shape: `path` parsed, `content` emptied and dropped. * A call carrying a real-looking `path` is the more dangerous of the two — a * client that only checks "did I get a call" would run it. */ static void test_nonstream_body_refuses_partial_arguments(void) { jr_value *root = wire_emit_tool_turn_verdict( "\n\n/etc/hostname\n\n", SG_TOOL_GRAMMAR_INCOMPLETE); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); ASSERT_STR_EQ(jr_as_string(jr_object_get( jr_object_get(choice, "tool_call_error"), "code")), "call_incomplete"); jr_free(root); } /* CONTROL — the grammar CLOSED the call: untouched, and unflagged. Without this * the gate could be "refuse everything" and still look green. */ static void test_nonstream_body_keeps_complete_call(void) { jr_value *root = wire_emit_tool_turn_verdict( "\n\n\n/etc/hostname\n" "\n", SG_TOOL_GRAMMAR_COMPLETE); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); const jr_value *tcs = jr_object_get(jr_object_get(choice, "message"), "tool_calls"); ASSERT_NOT_NULL(tcs); ASSERT_INT_EQ(jr_array_len(tcs), 1); ASSERT_NULL(jr_object_get(choice, "tool_call_error")); jr_free(root); } /* CONTROL — NO tool grammar ran (PANTHEON_TOOL_GRAMMAR off, or a dialect this * grammar does not model, or a walk that never entered a call). ABSENT is not a * soft "incomplete": with no verdict to gate on, the turn must behave exactly as * it did before this gate existed. */ static void test_nonstream_body_ungated_without_a_grammar(void) { jr_value *root = wire_emit_tool_turn_verdict( "\n\n\n\n" "\n", SG_TOOL_GRAMMAR_ABSENT); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NOT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); ASSERT_NULL(jr_object_get(choice, "tool_call_error")); jr_free(root); } /* A reason the PARSER already named wins — it is the same event seen closer up, * and it carries the parameter name. The gate must not overwrite it. */ static void test_incomplete_does_not_mask_a_named_reason(void) { jr_value *root = wire_emit_tool_turn_verdict( "\n\n\n\n", SG_TOOL_GRAMMAR_INCOMPLETE); ASSERT_NOT_NULL(root); const jr_value *choice = wire_first_choice(root); ASSERT_NOT_NULL(choice); ASSERT_NULL(jr_object_get(jr_object_get(choice, "message"), "tool_calls")); const jr_value *err = jr_object_get(choice, "tool_call_error"); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "code")), "call_abandoned"); ASSERT_STR_EQ(jr_as_string(jr_object_get(err, "parameter")), "path"); jr_free(root); } /* ============================================================ * THE TOOL-SYNTAX LEAK. Everything in collect_tool_calls hangs off * `res->tool_raw`, which the emitter only fills once it recognises a real * opener. A model that invents a NEAR-MISS frame matches no opener, is never * diverted, and lands in `content` — so the turn ends with no call, no defect * code, and NOTHING in the log. That silence is what made this class of bug * cost a session to find: it reads exactly like a turn that chose to answer in * prose. * * The captured text is verbatim from qwen36-bf16 (BF16 full weights, 7 tools, * 558-token prompt, grammar off). stderr is redirected to a temp file so the * assertion is on what the fleet log actually receives, not on the call being * spelled right. * ============================================================ */ static char *capture_leak_stderr(const char *content, const char *tool_raw, int with_tools) { static char buf[4096]; char path[] = "/tmp/pn_tool_leak_XXXXXX"; int fd = mkstemp(path); if (fd < 0) return NULL; fflush(stderr); int saved = dup(fileno(stderr)); dup2(fd, fileno(stderr)); jr_value *tools = with_tools ? jr_parse("[{\"type\":\"function\",\"function\":{\"name\":\"bash\"," "\"parameters\":{\"type\":\"object\",\"properties\":" "{\"command\":{\"type\":\"string\"}}}}}]") : NULL; chat_request req = {0}; req.tools = tools; sg_result res = {0}; res.content = (char *)content; res.tool_raw = (char *)tool_raw; tc_tool_call *calls = NULL; int n = 0; tc_parse_error err = {0}; collect_tool_calls(&req, &res, &calls, &n, &err); tc_tool_calls_free(calls, n); tc_parse_error_free(&err); jr_free(tools); fflush(stderr); dup2(saved, fileno(stderr)); close(saved); lseek(fd, 0, SEEK_SET); ssize_t got = read(fd, buf, sizeof(buf) - 1); buf[got > 0 ? got : 0] = '\0'; close(fd); remove(path); return buf; } static const char INVENTED_FRAME[] = "\n\nbash\n\n" "command\n" "sed -n '175,200p' src/context/context_store.c" "\n\n"; static void test_invented_tool_frame_is_reported(void) { const char *log = capture_leak_stderr(INVENTED_FRAME, NULL, 1); ASSERT_TRUE(log != NULL); ASSERT_TRUE(strstr(log, "[tool_syntax_leak]") != NULL); /* The raw text has to be IN the line — the whole point is that the client * only ever records the parsed result, so this is the one place the bytes * that produced the empty turn still exist. */ ASSERT_TRUE(strstr(log, "bash") != NULL); } /* A plain prose answer is the common case and must stay silent, or the signal is * worthless. */ static void test_plain_prose_answer_is_silent(void) { const char *log = capture_leak_stderr( "The file reads a config and returns 0 on success.", NULL, 1); ASSERT_TRUE(log != NULL); ASSERT_TRUE(strstr(log, "[tool_syntax_leak]") == NULL); } /* A turn that DID divert a frame is `no_call_parsed`'s business, not this one: * the two silences are different and must not be conflated. */ static void test_diverted_frame_is_not_a_leak(void) { const char *log = capture_leak_stderr("", "\n\n", 1); ASSERT_TRUE(log != NULL); ASSERT_TRUE(strstr(log, "[tool_syntax_leak]") == NULL); ASSERT_TRUE(strstr(log, "no_call_parsed") != NULL); } /* No tools on the request → the model was never offered a call, and tool-ish * prose (a code answer ABOUT tool calling) is not a defect. */ static void test_no_tools_no_report(void) { const char *log = capture_leak_stderr(INVENTED_FRAME, NULL, 0); ASSERT_TRUE(log != NULL); ASSERT_TRUE(strstr(log, "[tool_syntax_leak]") == NULL); } /* ── tool calls stream WHILE they are generated (run_streaming end to end) ── * The stubbed generation feeds a real emitter whose tool tap is armed by * run_streaming, exactly as serve_generation.c does. The old behaviour sent * nothing of a call until the turn ended — the client's stall watchdog then * killed long `write` calls — so these assert on what was on the wire BEFORE * the end, and that the streamed fragments add up to the parsed arguments. */ typedef struct { char name[32]; json_writer args; int fragments; bool seen; } folded_call; /* Fold `len` bytes of SSE the way a client does (tool_calls by index). */ static int fold_wire(const char *wire, size_t len, folded_call *calls, int max, char *finish, size_t finish_len, int *keepalives) { int n = 0; *keepalives = 0; if (finish_len) finish[0] = '\0'; char *copy = strndup(wire, len); for (char *line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) { if (line[0] == ':') { (*keepalives)++; continue; } if (strncmp(line, "data: ", 6) != 0) continue; jr_value *ev = jr_parse(line + 6); const jr_value *ch = jr_array_get(jr_object_get(ev, "choices"), 0); const jr_value *tcs = jr_object_get(jr_object_get(ch, "delta"), "tool_calls"); for (int i = 0; i < jr_array_len(tcs); i++) { const jr_value *tc = jr_array_get(tcs, i); int idx = (int)jr_as_number(jr_object_get(tc, "index"), -1); if (idx < 0 || idx >= max) continue; folded_call *fc = &calls[idx]; if (!fc->seen) { fc->seen = true; jw_init(&fc->args); if (idx >= n) n = idx + 1; } const jr_value *fn = jr_object_get(tc, "function"); const char *name = jr_as_string(jr_object_get(fn, "name")); if (name) snprintf(fc->name, sizeof(fc->name), "%s", name); const char *args = jr_as_string(jr_object_get(fn, "arguments")); if (args && *args) { jw_raw(&fc->args, args); fc->fragments++; } } const char *fr = jr_as_string(jr_object_get(ch, "finish_reason")); if (fr && finish_len) snprintf(finish, finish_len, "%s", fr); jr_free(ev); } free(copy); return n; } static void free_folded(folded_call *calls, int n) { for (int i = 0; i < n; i++) if (calls[i].seen) jw_free(&calls[i].args); } #define TOOLS_WRITE ",\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"write\"," \ "\"parameters\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}," \ "\"content\":{\"type\":\"string\"}},\"required\":[\"path\",\"content\"]}}}]" static const char *const ORNITH_WRITE_PIECES[] = { "I'll write it.\n\n", "```", "json\n", "{\n \"", "function", "\": \"", "write", "\",\n \"", "arguments", "\": {\n \"path\": \"", "a.py", "\",\n \"content\": \"", "import os\\n", "\\n", "def main():\\n", " print(\\\"hi\\\")\\n", " return 0\\n", "\\n", "if __name__ == ", "\\\"__main__\\\":\\n", " main()\\n", "\"\n }\n}", "\n```", }; /* Run one streaming completion over `pieces`; leaves the wire in wire_all. */ static void run_stub_stream(chat_request *req, const char *const *pieces, int n, double keepalive_s) { jw_free(&wire_all); jw_init(&wire_all); stub_pieces = pieces; stub_n_pieces = n; stream_ctx sc = { .client_fd = -1, .id = "cmpl-tc", .created = 0, .model_id = "test-model", .keepalive_s = keepalive_s }; run_streaming(-1, NULL, NULL, req, &req->gen, &sc, NULL); stub_pieces = NULL; stub_n_pieces = 0; } static void test_stream_tool_call_reaches_wire_before_turn_ends(void) { chat_request req; jr_value *root = parse_body(BODY_HEAD TOOLS_WRITE ",\"stream\":true}", &req); ASSERT_NOT_NULL(root); req.gen.dialect = TC_DIALECT_ORNITH_JSON; /* what chat_apply_tool_dialect sets for ornith */ int n = (int)(sizeof(ORNITH_WRITE_PIECES) / sizeof(*ORNITH_WRITE_PIECES)); run_stub_stream(&req, ORNITH_WRITE_PIECES, n, 0); /* What the client had BEFORE generation ended: the opening delta and * argument fragments — not silence. */ folded_call early[4] = {0}; char fin[32]; int ka = 0; int n_early = fold_wire(wire_all.data, stub_wire_len_before_end, early, 4, fin, sizeof(fin), &ka); ASSERT_INT_EQ(n_early, 1); ASSERT_STR_EQ(early[0].name, "write"); ASSERT_TRUE(early[0].fragments >= 5); ASSERT_TRUE(strstr(early[0].args.data, "def main()") != NULL); /* The whole stream: one call whose fragments concatenate to the parsed * arguments (what the non-streaming body carries), finish tool_calls. */ folded_call all[4] = {0}; int n_all = fold_wire(wire_all.data, wire_all.len, all, 4, fin, sizeof(fin), &ka); ASSERT_INT_EQ(n_all, 1); json_writer raw; jw_init(&raw); for (int k = 0; k < n; k++) jw_raw(&raw, ORNITH_WRITE_PIECES[k]); tc_tool_call *calls = NULL; int n_calls = 0; tc_parse_tool_calls(strstr(raw.data, "```"), req.tools, TC_DIALECT_ORNITH_JSON, &calls, &n_calls); ASSERT_INT_EQ(n_calls, 1); ASSERT_STR_EQ(all[0].args.data, calls[0].arguments); ASSERT_STR_EQ(fin, "tool_calls"); tc_tool_calls_free(calls, n_calls); jw_free(&raw); free_folded(early, n_early); free_folded(all, n_all); chat_request_free(&req); jr_free(root); } /* A call that cannot be streamed early (the XML dialect) must still not leave * the wire silent: keepalive comments while it is written, the call itself * at the end exactly as before. */ static void test_stream_keepalive_while_call_is_held(void) { static const char *const pieces[] = { "\n", "\n", "\na.txt\n", "\n", "\nhello\n", "\n", "\n", "", }; chat_request req; jr_value *root = parse_body(BODY_HEAD TOOLS_WRITE ",\"stream\":true}", &req); ASSERT_NOT_NULL(root); run_stub_stream(&req, pieces, (int)(sizeof(pieces) / sizeof(*pieces)), 1e-9); folded_call early[4] = {0}; char fin[32]; int ka = 0; int n_early = fold_wire(wire_all.data, stub_wire_len_before_end, early, 4, fin, sizeof(fin), &ka); ASSERT_INT_EQ(n_early, 0); /* held: nothing of the call yet … */ ASSERT_TRUE(ka >= 1); /* … but the stream is not silent */ folded_call all[4] = {0}; int n_all = fold_wire(wire_all.data, wire_all.len, all, 4, fin, sizeof(fin), &ka); ASSERT_INT_EQ(n_all, 1); ASSERT_STR_EQ(all[0].name, "write"); ASSERT_STR_EQ(all[0].args.data, "{\"path\":\"a.txt\",\"content\":\"hello\"}"); ASSERT_STR_EQ(fin, "tool_calls"); free_folded(all, n_all); chat_request_free(&req); jr_free(root); } int main(void) { printf("\n=== chat_request (tool_choice parse + forced_name lifetime) ===\n\n"); RUN_TEST(forced_name_survives_dom_free); RUN_TEST(tool_choice_string_forms); RUN_TEST(session_key_aliases); RUN_TEST(required_no_forced_name); RUN_TEST(forced_without_tools_downgrades); RUN_TEST(malformed_object_form); printf("\n=== tool-syntax leak (a frame that matched no opener) ===\n\n"); RUN_TEST(invented_tool_frame_is_reported); RUN_TEST(plain_prose_answer_is_silent); RUN_TEST(diverted_frame_is_not_a_leak); RUN_TEST(no_tools_no_report); printf("\n=== openai fields (response_format / seed / penalties / bias / logprobs) ===\n\n"); RUN_TEST(response_format_json_object_is_applied); RUN_TEST(response_format_text_is_a_noop); RUN_TEST(response_format_absent_is_a_noop); RUN_TEST(response_format_json_schema_downgrades); RUN_TEST(response_format_unknown_type_is_reported); RUN_TEST(response_format_yields_to_tools); RUN_TEST(response_format_applies_when_tool_choice_none); RUN_TEST(seed_is_parsed); RUN_TEST(enable_thinking_is_parsed); RUN_TEST(penalties_are_parsed_and_clamped); RUN_TEST(logit_bias_is_parsed_and_owned); RUN_TEST(logprobs_are_parsed); RUN_TEST(nonstream_reports_response_format_status); RUN_TEST(nonstream_omits_status_when_not_requested); RUN_TEST(stream_chunk_reports_response_format_status); RUN_TEST(stream_chunk_omits_status_when_not_requested); RUN_TEST(run_streaming_reports_enforced_status); RUN_TEST(run_streaming_reports_unenforced_status); RUN_TEST(n_defaults_to_one); RUN_TEST(n_is_honoured); RUN_TEST(n_over_the_ceiling_is_reported_not_silent); RUN_TEST(n_zero_and_negative_floor_at_one); RUN_TEST(seeded_choices_get_distinct_streams); RUN_TEST(nonstream_emits_all_choices); RUN_TEST(nonstream_reports_n_shortfall); RUN_TEST(stream_chunk_carries_the_choice_index); RUN_TEST(status_applied_requires_actual_enforcement); RUN_TEST(status_schema_downgrade_also_requires_enforcement); RUN_TEST(status_ignored_reasons_pass_through); RUN_TEST(status_absent_stays_absent); RUN_TEST(nonstream_body_carries_not_enforced); RUN_TEST(nonstream_body_reports_abandoned_call); RUN_TEST(nonstream_body_keeps_empty_argument_call); RUN_TEST(nonstream_body_silent_when_no_call); RUN_TEST(nonstream_body_reports_unparseable_frame); RUN_TEST(nonstream_body_repairs_truncated_json_call); RUN_TEST(nonstream_body_reports_unknown_tool_name); RUN_TEST(nonstream_body_clean_call_unflagged); RUN_TEST(nonstream_body_refuses_incomplete_call); RUN_TEST(nonstream_body_refuses_partial_arguments); RUN_TEST(nonstream_body_keeps_complete_call); RUN_TEST(nonstream_body_ungated_without_a_grammar); RUN_TEST(incomplete_does_not_mask_a_named_reason); printf("\n=== chat response shape (session_conflict on the wire) ===\n\n"); RUN_TEST(nonstream_usage_carries_session_conflict); RUN_TEST(nonstream_usage_omits_session_conflict_when_clean); RUN_TEST(stream_usage_chunk_carries_session_conflict); RUN_TEST(stream_usage_chunk_omits_session_conflict_when_clean); printf("\n=== chat response shape (cached_tokens on the wire) ===\n\n"); RUN_TEST(nonstream_usage_reports_cached_tokens); RUN_TEST(nonstream_usage_reports_zero_cached_tokens_when_cold); RUN_TEST(stream_usage_chunk_reports_cached_tokens); RUN_TEST(stream_usage_chunk_reports_zero_cached_tokens_when_cold); RUN_TEST(stream_tool_call_reaches_wire_before_turn_ends); RUN_TEST(stream_keepalive_while_call_is_held); TEST_REPORT(); return __ft_failed == 0 ? 0 : 1; }