#include "provider.h" #include #include #include #include "config.h" #include "dotagents.h" #include "message.h" #include "session.h" static int failures = 0; static int checks = 0; #define CHECK(cond, msg) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ } \ } while (0) #define CHECK_CONT(hay, needle, msg) \ do { \ checks++; \ if (!hay || !strstr(hay, needle)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s (want substring `%s`)\n", \ __FILE__, __LINE__, (msg), (needle)); \ } \ } while (0) #define CHECK_NOT_CONT(hay, needle, msg) \ do { \ checks++; \ if (hay && strstr(hay, needle)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s (found unwanted `%s`)\n", \ __FILE__, __LINE__, (msg), (needle)); \ } \ } while (0) static const char *SENTINEL = "SENTINEL-PREAMBLE-XYZ"; static void mock_destroy(llsh_model *m) { (void)m; } static llsh_model MOCK = {NULL, NULL, mock_destroy}; static session *make_session(const char *provider) { config c; memset(&c, 0, sizeof c); c.provider = provider_parse(provider, NULL); c.model = xstrdup("test-model"); c.preamble = xstrdup(SENTINEL); dotagents d; memset(&d, 0, sizeof d); return session_new_with_model(&MOCK, c, d); } /* The preamble must reach the wire for every provider. Before the fix it was * only emitted for Anthropic and silently dropped for OpenAI/Ollama. */ static void test_preamble_openai(void) { session *s = make_session("openai"); char *body = provider_preview_request(s, "hello", true); CHECK(body != NULL, "openai body built"); /* OpenAI carries the system prompt as a leading system message. */ CHECK_CONT(body, "\"role\":\"system\"", "has a system message"); CHECK_CONT(body, SENTINEL, "system message carries the preamble"); CHECK_CONT(body, "\"role\":\"user\"", "user message still present"); CHECK_CONT(body, "hello", "prompt present"); free(body); session_free(s); } static void test_preamble_ollama(void) { session *s = make_session("ollama"); char *body = provider_preview_request(s, "hello", true); CHECK(body != NULL, "ollama body built"); /* Ollama carries the system prompt as a top-level `system` field. */ CHECK_CONT(body, "\"system\":", "has a top-level system field"); CHECK_CONT(body, SENTINEL, "system field carries the preamble"); CHECK_CONT(body, "hello", "prompt present"); free(body); session_free(s); } static void test_preamble_anthropic(void) { session *s = make_session("anthropic"); char *body = provider_preview_request(s, "hello", true); CHECK(body != NULL, "anthropic body built"); CHECK_CONT(body, "\"system\":", "has a system field"); CHECK_CONT(body, SENTINEL, "system carries the preamble"); free(body); session_free(s); } /* A model name with JSON-special characters must not corrupt the body. */ static void test_model_name_escaped(void) { config c; memset(&c, 0, sizeof c); c.provider = PROV_OPENAI; c.model = xstrdup("we\"ird/model"); c.preamble = xstrdup(SENTINEL); dotagents d; memset(&d, 0, sizeof d); session *s = session_new_with_model(&MOCK, c, d); char *body = provider_preview_request(s, "hi", false); CHECK(body != NULL, "body built"); /* The raw `"` in the model name must be escaped to \". */ CHECK_CONT(body, "we\\\"ird/model", "model name quote is escaped"); free(body); session_free(s); } static void test_no_preamble_omits_system(void) { config c; memset(&c, 0, sizeof c); c.provider = PROV_OPENAI; c.model = xstrdup("m"); c.preamble = NULL; /* no preamble */ dotagents d; memset(&d, 0, sizeof d); session *s = session_new_with_model(&MOCK, c, d); char *body = provider_preview_request(s, "hi", false); CHECK(body != NULL, "body built"); CHECK_NOT_CONT(body, "\"role\":\"system\"", "no system message without preamble"); free(body); session_free(s); } /* A turn stopped mid tool-loop leaves an assistant message with more tool * calls than the tool-result messages that follow it. The trim must drop the * surplus calls so the history stays a valid request. */ static void test_trim_dangling_calls(void) { message am = msg_assistant_text("working on it"); msg_push_assistant_tool_call(am, "read_file", "{\"path\":\"a\"}"); msg_push_assistant_tool_call(am, "write_file", "{\"path\":\"b\"}"); message r1 = msg_user_tool_result("call-1", "read_file", "contents"); message msgs[2] = {am, r1}; CHECK(am.assistant().parts.size() == 3, "assistant has text + 2 calls before trim"); provider_trim_dangling_calls(msgs, 2); int calls = 0; for (size_t i = 0; i < msgs[0].assistant().parts.size(); i++) if (std::holds_alternative( msgs[0].assistant().parts[i])) calls++; CHECK(calls == 1, "surplus call trimmed to match the single result"); /* Fully-answered calls must be untouched. */ message am2 = msg_assistant_text(""); msg_push_assistant_tool_call(am2, "read_file", "{}"); message ra = msg_user_tool_result("c1", "read_file", "x"); message rb = msg_user_tool_result("c2", "write_file", "ok"); msg_push_assistant_tool_call(am2, "write_file", "{}"); message msgs2[3] = {am2, ra, rb}; provider_trim_dangling_calls(msgs2, 3); int calls2 = 0; for (size_t i = 0; i < msgs2[0].assistant().parts.size(); i++) if (std::holds_alternative( msgs2[0].assistant().parts[i])) calls2++; CHECK(calls2 == 2, "complete call/result pair is preserved"); } /* Reasoning models return the chain of thought in provider-specific fields. * None of the parsers extracted it before the fix, so it was never displayed. */ static void test_reasoning_openai(void) { const char *body = "{\"choices\":[{\"message\":{\"content\":\"42\"," "\"reasoning_content\":\"think... think... 42\"}}]}"; llm_conn conn; memset(&conn, 0, sizeof conn); conn.provider = LLM_OPENAI; conn.model = "m"; llm_completion comp; memset(&comp, 0, sizeof comp); CHECK(llm_parse_body(&conn, body, &comp) == 0, "openai body parses"); CHECK_CONT(comp.text, "42", "answer text extracted"); CHECK(comp.reasoning != NULL, "reasoning extracted (was NULL before fix)"); CHECK_CONT(comp.reasoning, "think...", "reasoning content preserved"); llm_completion_free(&comp); } static void test_reasoning_ollama(void) { const char *body = "{\"message\":{\"content\":\"the answer\"," "\"thinking\":\"pondering the answer...\"}}"; llm_conn conn; memset(&conn, 0, sizeof conn); conn.provider = LLM_OLLAMA; conn.model = "m"; llm_completion comp; memset(&comp, 0, sizeof comp); CHECK(llm_parse_body(&conn, body, &comp) == 0, "ollama body parses"); CHECK_CONT(comp.text, "the answer", "answer text extracted"); CHECK(comp.reasoning != NULL, "reasoning extracted from message.thinking"); CHECK_CONT(comp.reasoning, "pondering", "reasoning content preserved"); llm_completion_free(&comp); } static void test_reasoning_anthropic(void) { const char *body = "{\"content\":[{\"type\":\"thinking\",\"thinking\":\"hmm, let me " "reason...\"},{\"type\":\"text\",\"text\":\"done\"}]}"; llm_conn conn; memset(&conn, 0, sizeof conn); conn.provider = LLM_ANTHROPIC; conn.model = "m"; llm_completion comp; memset(&comp, 0, sizeof comp); CHECK(llm_parse_body(&conn, body, &comp) == 0, "anthropic body parses"); CHECK_CONT(comp.text, "done", "answer text extracted"); CHECK(comp.reasoning != NULL, "reasoning extracted from thinking block"); CHECK_CONT(comp.reasoning, "reason", "reasoning content preserved"); llm_completion_free(&comp); } /* A plain (non-reasoning) response must not fabricate a reasoning field. */ static void test_no_reasoning_when_absent(void) { const char *body = "{\"choices\":[{\"message\":{\"content\":\"just an answer\"}}]}"; llm_conn conn; memset(&conn, 0, sizeof conn); conn.provider = LLM_OPENAI; conn.model = "m"; llm_completion comp; memset(&comp, 0, sizeof comp); CHECK(llm_parse_body(&conn, body, &comp) == 0, "plain body parses"); CHECK_CONT(comp.text, "just an answer", "answer text extracted"); CHECK(comp.reasoning == NULL, "no reasoning field fabricated"); llm_completion_free(&comp); } /* ------------------------------------------------------------------ */ /* sse_parse_line -- the pure streaming unit (AUDIT phase 2 test seam) */ /* ------------------------------------------------------------------ */ /* OpenAI: text deltas, two interleaved multi-chunk tool calls, * finish_reason, [DONE], usage. */ static void test_sse_openai(void) { llm_sse_acc a; llm_sse_acc_init(&a, LLM_OPENAI); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{\"content\":\"Hel\"}}]}", &a); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{\"content\":\"lo\"}}]}", &a); CHECK(a.text.len == 5, "text deltas accumulate"); CHECK_CONT(a.text.data, "Hello", "text content"); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{\"tool_calls\":[" "{\"index\":0,\"id\":\"c1\",\"function\":{\"name\":\"read_file\"," "\"arguments\":\"{\\\"pa\"}}]}}]}", &a); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{\"tool_calls\":[" "{\"index\":1,\"id\":\"c2\",\"function\":{\"name\":\"grep\"," "\"arguments\":\"{\\\"pat\"}}]}}]}", &a); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{\"tool_calls\":[" "{\"index\":0,\"function\":{\"arguments\":\"th\\\":\\\"a.txt\\\"}\"}}]}}]}", &a); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{\"tool_calls\":[" "{\"index\":1,\"function\":{\"arguments\":\"tern\\\":\\\"x\\\"}\"}}]}}]}", &a); CHECK(a.tc_len == 2, "two tool calls indexed"); CHECK(a.tc_id[0] && strcmp(a.tc_id[0], "c1") == 0, "call 0 id"); CHECK(a.tc_id[1] && strcmp(a.tc_id[1], "c2") == 0, "call 1 id"); CHECK(a.tc_name[0] && strcmp(a.tc_name[0], "read_file") == 0, "call 0 name"); CHECK_CONT(a.tc_args[0].data, "\"path\":\"a.txt\"", "call 0 args assembled"); CHECK_CONT(a.tc_args[1].data, "\"pattern\":\"x\"", "call 1 args assembled"); llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}]}", &a); CHECK(a.done, "finish_reason marks the stream done"); llm_sse_parse_line(LLM_OPENAI, "data: [DONE]", &a); llm_sse_parse_line(LLM_OPENAI, "data: {\"usage\":{\"prompt_tokens\":42}}", &a); CHECK(a.input_tokens == 42, "usage.prompt_tokens captured"); llm_sse_acc_free(&a); } /* Anthropic: event:/data: pairing, thinking + text blocks, a tool_use * block with chunked input_json_delta, message_start usage, message_stop. */ static void test_sse_anthropic(void) { llm_sse_acc a; llm_sse_acc_init(&a, LLM_ANTHROPIC); llm_sse_parse_line(LLM_ANTHROPIC, "event: message_start", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"message\":{\"usage\":{\"input_tokens\":7}}}", &a); CHECK(a.input_tokens == 7, "usage from message_start"); llm_sse_parse_line(LLM_ANTHROPIC, "event: content_block_start", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"content_block\":{\"type\":\"thinking\"}}", &a); llm_sse_parse_line(LLM_ANTHROPIC, "event: content_block_delta", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"hmm \"}}", &a); llm_sse_parse_line(LLM_ANTHROPIC, "event: content_block_start", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"content_block\":{\"type\":\"text\"}}", &a); llm_sse_parse_line(LLM_ANTHROPIC, "event: content_block_delta", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"delta\":{\"type\":\"text_delta\",\"text\":\"ok\"}}", &a); CHECK_CONT(a.reasoning.data, "hmm", "thinking_delta captured"); CHECK_CONT(a.text.data, "ok", "text_delta captured"); llm_sse_parse_line(LLM_ANTHROPIC, "event: content_block_start", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"content_block\":{\"type\":\"tool_use\",\"id\":\"t1\"," "\"name\":\"write_file\"}}", &a); llm_sse_parse_line(LLM_ANTHROPIC, "event: content_block_delta", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"delta\":{\"type\":\"input_json_delta\"," "\"partial_json\":\"{\\\"pa\"}}", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"delta\":{\"type\":\"input_json_delta\"," "\"partial_json\":\"th\\\":\\\"x\\\"}\"}}", &a); CHECK(a.tc_len == 1, "one tool_use block"); CHECK(a.tc_id[0] && strcmp(a.tc_id[0], "t1") == 0, "tool_use id"); CHECK(a.tc_name[0] && strcmp(a.tc_name[0], "write_file") == 0, "tool_use name"); CHECK_CONT(a.tc_args[0].data, "\"path\":\"x\"", "chunked partial_json assembled"); llm_sse_parse_line(LLM_ANTHROPIC, "event: message_stop", &a); llm_sse_parse_line(LLM_ANTHROPIC, "data: [DONE]", &a); CHECK(a.done, "message_stop marks done"); llm_sse_acc_free(&a); } /* Ollama NDJSON: content + thinking chunks, prompt_eval_count, tool_calls * on the done chunk (object arguments), done:true. */ static void test_sse_ollama(void) { llm_sse_acc a; llm_sse_acc_init(&a, LLM_OLLAMA); llm_sse_parse_line(LLM_OLLAMA, "{\"message\":{\"content\":\"He\"}}", &a); llm_sse_parse_line(LLM_OLLAMA, "{\"message\":{\"thinking\":\"think\"}}", &a); llm_sse_parse_line(LLM_OLLAMA, "{\"message\":{\"content\":\"llo\"}}", &a); CHECK_CONT(a.text.data, "Hello", "content chunks accumulate"); CHECK_CONT(a.reasoning.data, "think", "thinking chunk captured"); llm_sse_parse_line(LLM_OLLAMA, "{\"prompt_eval_count\":9}", &a); CHECK(a.input_tokens == 9, "prompt_eval_count captured"); llm_sse_parse_line(LLM_OLLAMA, "{\"message\":{\"tool_calls\":[{\"id\":\"o1\",\"function\":" "{\"name\":\"list_dir\",\"arguments\":{\"path\":\".\"}}}]}," "\"done\":true}", &a); CHECK(a.tc_len == 1, "tool call on the done chunk"); CHECK(a.tc_id[0] && strcmp(a.tc_id[0], "o1") == 0, "tool id"); CHECK(a.tc_name[0] && strcmp(a.tc_name[0], "list_dir") == 0, "tool name"); CHECK_CONT(a.tc_args[0].data, "\"path\":\".\"", "object args serialized"); CHECK(a.done, "done:true marks done"); llm_sse_acc_free(&a); } /* Lines outside the protocol (stray text, bad JSON) are ignored without * corrupting the accumulator. */ static void test_sse_noise_lines(void) { llm_sse_acc a; llm_sse_acc_init(&a, LLM_OPENAI); llm_sse_parse_line(LLM_OPENAI, "event: ping", &a); /* not `data:` -> ignored */ llm_sse_parse_line(LLM_OPENAI, "data: not-json", &a); /* bad JSON -> ignored */ llm_sse_parse_line(LLM_OPENAI, "data: {\"choices\":[]}", &a); /* empty choices */ CHECK(a.text.len == 0 && a.tc_len == 0 && !a.done, "noise left acc empty"); llm_sse_acc_free(&a); llm_sse_acc b; llm_sse_acc_init(&b, LLM_ANTHROPIC); llm_sse_parse_line(LLM_ANTHROPIC, "data: {\"no\":\"event\"}", &b); /* no event set */ llm_sse_parse_line(LLM_ANTHROPIC, "data: {", &b); /* truncated JSON */ CHECK(b.text.len == 0 && b.tc_len == 0 && !b.done, "anthropic noise ignored"); llm_sse_acc_free(&b); llm_sse_acc c; llm_sse_acc_init(&c, LLM_OLLAMA); llm_sse_parse_line(LLM_OLLAMA, "hello", &c); /* not JSON */ llm_sse_parse_line(LLM_OLLAMA, "{}", &c); /* no message field */ CHECK(c.text.len == 0 && c.tc_len == 0 && !c.done, "ollama noise ignored"); llm_sse_acc_free(&c); } /* sse_acc_into: unnamed/unargued calls get default ids and `{}` args. */ static void test_sse_acc_into_defaults(void) { llm_sse_acc a; llm_sse_acc_init(&a, LLM_OLLAMA); llm_acc_push_tool(&a, NULL, NULL); llm_completion c; memset(&c, 0, sizeof c); llm_sse_acc_into(&a, &c); CHECK(c.tc_len == 1, "one materialized call"); CHECK(c.tc_id[0] && strstr(c.tc_id[0], "call-1") != NULL, "default id"); CHECK(c.tc_args[0] && strcmp(c.tc_args[0], "{}") == 0, "default empty args"); llm_completion_free(&c); llm_sse_acc_free(&a); } /* sse_acc_into: non-empty text/reasoning transfer ownership to `c` and must * survive completion_free + sse_acc_free without a double-free. Regression: * the old code freed the stolen buffer right after handing it to `c`, * dangling c->text/c->reasoning (crashed on real streamed responses). */ static void test_sse_acc_into_text_reasoning_ownership(void) { llm_sse_acc a; llm_sse_acc_init(&a, LLM_OPENAI); llm_buf_put(&a.text, "the answer is 42"); llm_buf_put(&a.reasoning, "pondering deeply"); llm_acc_push_tool(&a, xstrdup("call-9"), xstrdup("read_file")); llm_buf_put(&a.tc_args[0], "\"path\":\"AUDIT.md\""); llm_completion c; memset(&c, 0, sizeof c); llm_sse_acc_into(&a, &c); CHECK(c.text && strcmp(c.text, "the answer is 42") == 0, "text transferred intact"); CHECK(c.reasoning && strcmp(c.reasoning, "pondering deeply") == 0, "reasoning transferred intact"); CHECK(c.tc_len == 1 && c.tc_args[0] && strcmp(c.tc_args[0], "\"path\":\"AUDIT.md\"") == 0, "tool args transferred intact"); /* Both frees must be safe: `c` owns the strings now, `a` is reset. */ llm_completion_free(&c); llm_sse_acc_free(&a); } /* ================= llm_params -> request body ================= * The `llm` tool threads llm_params (temperature, max_tokens, reasoning * effort, thinking budget, response schema) into the wire body. These are * pure request-builder tests (no network) per provider. */ static llm_message one_user(const char *text) { static llm_part part; memset(&part, 0, sizeof part); part.kind = LLM_TEXT; part.text = text; llm_message m; memset(&m, 0, sizeof m); m.role = LLM_ROLE_USER; m.parts = ∂ m.part_count = 1; return m; } static void test_params_openai(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_OPENAI; c.model = "test-model"; llm_params p; llm_params_init(&p); p.temperature = 0.5; p.max_tokens = 256; p.reasoning_effort = "high"; p.response_schema = "{\"type\":\"object\"}"; llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, NULL, &m, 1, NULL, false, &p); CHECK(body != NULL, "openai body built"); if (body) { CHECK_CONT(body, "\"temperature\":0.5", "openai temperature"); CHECK_CONT(body, "\"max_tokens\":256", "openai max_tokens"); CHECK_CONT(body, "\"reasoning_effort\":\"high\"", "openai reasoning_effort"); CHECK_CONT(body, "\"response_format\":{\"type\":\"json_schema\"", "openai response_format"); CHECK_CONT(body, "\"type\":\"object\"", "openai schema inlined"); free(body); } } static void test_params_ollama(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_OLLAMA; c.model = "ollama-model"; llm_params p; llm_params_init(&p); p.temperature = 0.75; /* 3/4: exact in binary, so %.6g is stable */ p.max_tokens = 128; p.reasoning_effort = "medium"; p.response_schema = "{\"type\":\"object\"}"; llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, "SYS", &m, 1, NULL, false, &p); CHECK(body != NULL, "ollama body built"); if (body) { CHECK_CONT(body, "\"system\":\"SYS\"", "ollama top-level system"); CHECK_CONT(body, "\"options\":{\"temperature\":0.75", "ollama options.temperature"); CHECK_CONT(body, "\"num_predict\":128", "ollama num_predict"); CHECK_CONT(body, "\"think\":true", "ollama think enabled"); CHECK_CONT(body, "\"format\":", "ollama format field"); CHECK_CONT(body, "\"type\":\"object\"", "ollama schema"); free(body); } } static void test_params_anthropic_thinking(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_ANTHROPIC; c.model = "claude"; llm_params p; llm_params_init(&p); p.thinking_budget = 16384; llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, NULL, &m, 1, NULL, false, &p); CHECK(body != NULL, "anthropic body built"); if (body) { CHECK_CONT(body, "\"thinking\":{\"type\":\"enabled\"," "\"budget_tokens\":16384}", "anthropic thinking budget"); /* max_tokens must be bumped above the thinking budget. */ CHECK_CONT(body, "\"max_tokens\":17408", "anthropic max_tokens > budget"); free(body); } } static void test_params_anthropic_effort(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_ANTHROPIC; c.model = "claude"; llm_params p; llm_params_init(&p); p.reasoning_effort = "low"; /* maps to a 1024-token budget */ llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, NULL, &m, 1, NULL, false, &p); CHECK(body != NULL, "anthropic body built"); if (body) CHECK_CONT(body, "\"budget_tokens\":1024", "effort 'low' maps to a 1024 budget"); free(body); } static void test_params_anthropic_temperature(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_ANTHROPIC; c.model = "claude"; llm_params p; llm_params_init(&p); p.temperature = 0.25; /* 1/4: exact in binary, no thinking budget */ llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, NULL, &m, 1, NULL, false, &p); CHECK(body != NULL, "anthropic body built"); if (body) { CHECK_CONT(body, "\"temperature\":0.25", "anthropic temperature"); CHECK_NOT_CONT(body, "\"thinking\"", "no thinking block without a budget"); free(body); } } static void test_params_anthropic_schema(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_ANTHROPIC; c.model = "claude"; llm_params p; llm_params_init(&p); p.response_schema = "{\"type\":\"object\"}"; llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, "SYS", &m, 1, NULL, false, &p); CHECK(body != NULL, "anthropic body built"); if (body) { CHECK_CONT(body, "SYS", "preamble kept"); CHECK_CONT(body, "JSON Schema", "schema instruction appended"); /* The schema is JSON-encoded into the system string, so its quotes * are escaped in the body. */ CHECK_CONT(body, "\\\"type\\\"", "schema text embedded (escaped)"); CHECK_NOT_CONT(body, "response_format", "anthropic has no response_format"); free(body); } } static void test_params_unset(void) { llm_conn c; memset(&c, 0, sizeof c); c.provider = LLM_OPENAI; c.model = "m"; llm_message m = one_user("hi"); char *body = llm_build_request_ex(&c, NULL, &m, 1, NULL, false, NULL); CHECK(body != NULL, "body built with NULL params"); if (body) { CHECK_NOT_CONT(body, "temperature", "no temperature when unset"); CHECK_NOT_CONT(body, "max_tokens", "no max_tokens when unset"); CHECK_NOT_CONT(body, "response_format", "no response_format when unset"); free(body); } } /* ================= `compact` built-in tool ================= * session_force_compact is the dispatch target for the `compact` tool call. */ static void test_force_compact_short(void) { session *s = make_session("openai"); session_push_history(s, msg_user_text("hello")); session_push_history(s, msg_assistant_text("hi")); compact_report rep; memset(&rep, 0, sizeof rep); char *err = NULL; int rc = session_force_compact(s, &rep, &err); /* user+assistant only: keep-boundary at index 0 -> no pass can run. */ CHECK(rc == 0, "no pass on a too-short history"); free(err); session_free(s); } static void test_force_compact_clears(void) { session *s = make_session("openai"); session_push_history(s, msg_user_text("read the file")); session_push_history(s, msg_assistant_tool_call("read_file", "{}")); session_push_history(s, msg_user_tool_result("c1", "read_file", "a lot of cleared content")); session_push_history(s, msg_assistant_text("done reading")); session_push_history(s, msg_user_text("now edit it")); compact_report rep; memset(&rep, 0, sizeof rep); char *err = NULL; int rc = session_force_compact(s, &rep, &err); CHECK(rc == 1, "compaction pass ran"); CHECK(rep.tier == TIER_FALLBACK_CLEAR, "no summarizer -> fallback clear tier"); CHECK(rep.cleared_tool_results >= 1, "stale tool result cleared"); free(err); session_free(s); } int main(void) { test_preamble_openai(); test_preamble_ollama(); test_preamble_anthropic(); test_model_name_escaped(); test_no_preamble_omits_system(); test_trim_dangling_calls(); test_reasoning_openai(); test_reasoning_ollama(); test_reasoning_anthropic(); test_no_reasoning_when_absent(); test_sse_openai(); test_sse_anthropic(); test_sse_ollama(); test_sse_noise_lines(); test_sse_acc_into_defaults(); test_sse_acc_into_text_reasoning_ownership(); test_params_openai(); test_params_ollama(); test_params_anthropic_thinking(); test_params_anthropic_effort(); test_params_anthropic_temperature(); test_params_anthropic_schema(); test_params_unset(); test_force_compact_short(); test_force_compact_clears(); printf("provider: %d checks, %d failures\n", checks, failures); return failures ? 1 : 0; }