#include "ds4.h" #include "ds4_tool_text.h" #include "ds4_distributed.h" #include "ds4_gpu_args.h" #include "ds4_help.h" #include "ds4_kvstore.h" #include "ds4_tp.h" #include "rax.h" /* OpenAI/Anthropic compatible local server. * * HTTP is intentionally simple: each client connection is handled by a small * blocking thread that parses one request, then queues a job to a resident * session worker. A model coordinator batches decode-ready sessions and * serializes bounded prefill quanta, keeping graph mutations out of client * threads while preserving per-session KV ownership. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static volatile sig_atomic_t g_stop_requested = 0; static volatile sig_atomic_t g_listen_fd = -1; #define DS4_SERVER_IO_TIMEOUT_SEC 10 #define DS4_SERVER_SEND_STALL_TIMEOUT_MS 2000 #if defined(__GNUC__) || defined(__clang__) #define DS4_SERVER_MAYBE_UNUSED __attribute__((unused)) #else #define DS4_SERVER_MAYBE_UNUSED #endif static void stop_signal_handler(int sig) { (void)sig; if (g_stop_requested) _exit(130); g_stop_requested = 1; if (g_listen_fd >= 0) { int fd = (int)g_listen_fd; g_listen_fd = -1; close(fd); } } typedef struct { char *ptr; size_t len; size_t cap; } buf; static void die(const char *msg) { fprintf(stderr, "ds4-server: %s\n", msg); exit(1); } static void *xmalloc(size_t n) { void *p = malloc(n ? n : 1); if (!p) die("out of memory"); return p; } static void *xrealloc(void *p, size_t n) { p = realloc(p, n ? n : 1); if (!p) die("out of memory"); return p; } static char *xstrdup(const char *s) { size_t n = strlen(s); char *p = xmalloc(n + 1); memcpy(p, s, n + 1); return p; } static bool random_bytes(void *dst, size_t len) { unsigned char *p = dst; int fd = open("/dev/urandom", O_RDONLY); if (fd < 0) return false; while (len) { ssize_t n = read(fd, p, len); if (n < 0 && errno == EINTR) continue; if (n <= 0) { close(fd); return false; } p += (size_t)n; len -= (size_t)n; } close(fd); return true; } static char *xstrndup(const char *s, size_t n) { char *p = xmalloc(n + 1); memcpy(p, s, n); p[n] = '\0'; return p; } static void buf_reserve(buf *b, size_t add) { if (add > SIZE_MAX - b->len - 1) die("buffer overflow"); size_t need = b->len + add + 1; if (need <= b->cap) return; size_t cap = b->cap ? b->cap * 2 : 256; while (cap < need) { if (cap > SIZE_MAX / 2) { cap = need; break; } cap *= 2; } b->ptr = xrealloc(b->ptr, cap); b->cap = cap; } static void buf_append(buf *b, const void *p, size_t n) { buf_reserve(b, n); memcpy(b->ptr + b->len, p, n); b->len += n; b->ptr[b->len] = '\0'; } static void buf_putc(buf *b, char c) { buf_append(b, &c, 1); } static void buf_puts(buf *b, const char *s) { buf_append(b, s, strlen(s)); } static void buf_printf(buf *b, const char *fmt, ...) { va_list ap; va_start(ap, fmt); va_list ap2; va_copy(ap2, ap); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); if (n < 0) die("vsnprintf failed"); buf_reserve(b, (size_t)n); vsnprintf(b->ptr + b->len, b->cap - b->len, fmt, ap2); va_end(ap2); b->len += (size_t)n; } static char *buf_take(buf *b) { if (!b->ptr) return xstrdup(""); char *p = b->ptr; memset(b, 0, sizeof(*b)); return p; } static void buf_free(buf *b) { free(b->ptr); memset(b, 0, sizeof(*b)); } static void json_ws(const char **p) { while (**p && isspace((unsigned char)**p)) (*p)++; } static bool json_lit(const char **p, const char *lit) { size_t n = strlen(lit); if (strncmp(*p, lit, n) != 0) return false; *p += n; return true; } static int json_hex(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return 10 + c - 'a'; if (c >= 'A' && c <= 'F') return 10 + c - 'A'; return -1; } static void utf8_put(buf *b, uint32_t cp) { if (cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) cp = 0xfffd; if (cp <= 0x7f) { buf_putc(b, (char)cp); } else if (cp <= 0x7ff) { buf_putc(b, (char)(0xc0 | (cp >> 6))); buf_putc(b, (char)(0x80 | (cp & 0x3f))); } else if (cp <= 0xffff) { buf_putc(b, (char)(0xe0 | (cp >> 12))); buf_putc(b, (char)(0x80 | ((cp >> 6) & 0x3f))); buf_putc(b, (char)(0x80 | (cp & 0x3f))); } else { buf_putc(b, (char)(0xf0 | (cp >> 18))); buf_putc(b, (char)(0x80 | ((cp >> 12) & 0x3f))); buf_putc(b, (char)(0x80 | ((cp >> 6) & 0x3f))); buf_putc(b, (char)(0x80 | (cp & 0x3f))); } } static bool json_u16(const char **p, uint32_t *out) { if ((*p)[0] != '\\' || (*p)[1] != 'u') return false; uint32_t cp = 0; for (int i = 0; i < 4; i++) { int h = json_hex((*p)[2 + i]); if (h < 0) return false; cp = (cp << 4) | (uint32_t)h; } *p += 6; *out = cp; return true; } static bool json_string(const char **p, char **out) { /* Always define *out. Every failure path below returns false without * producing a string, and several callers reparse in place with * `free(x); json_string(&p, &x)` (e.g. duplicate JSON keys, the "model" * field). Without this, a non-string or malformed value leaves *out * holding the just-freed pointer, which a later cleanup frees again -- * a double-free. Nulling on entry closes that whole class at the root. */ *out = NULL; json_ws(p); if (**p != '"') return false; (*p)++; buf b = {0}; while (**p && **p != '"') { unsigned char c = (unsigned char)*(*p)++; if (c != '\\') { buf_putc(&b, (char)c); continue; } c = (unsigned char)*(*p)++; switch (c) { case '"': buf_putc(&b, '"'); break; case '\\': buf_putc(&b, '\\'); break; case '/': buf_putc(&b, '/'); break; case 'b': buf_putc(&b, '\b'); break; case 'f': buf_putc(&b, '\f'); break; case 'n': buf_putc(&b, '\n'); break; case 'r': buf_putc(&b, '\r'); break; case 't': buf_putc(&b, '\t'); break; case 'u': { *p -= 2; uint32_t cp = 0, lo = 0; if (!json_u16(p, &cp)) goto fail; if (cp >= 0xd800 && cp <= 0xdbff) { const char *low_start = *p; if (json_u16(p, &lo) && lo >= 0xdc00 && lo <= 0xdfff) { cp = 0x10000u + ((cp - 0xd800u) << 10) + (lo - 0xdc00u); } else { *p = low_start; cp = 0xfffd; } } utf8_put(&b, cp); break; } default: goto fail; } } if (**p != '"') goto fail; (*p)++; *out = buf_take(&b); return true; fail: buf_free(&b); return false; } static bool json_number(const char **p, double *out) { json_ws(p); char *end = NULL; double v = strtod(*p, &end); if (end == *p) return false; *p = end; *out = v; return true; } static bool json_int(const char **p, int *out) { double v = 0.0; if (!json_number(p, &v)) return false; /* json_number() uses strtod(), which accepts "NaN"/"Infinity". NaN fails * every comparison, so a plain `v < 0` clamp would let it through to the * (int) cast, which is undefined for NaN; `!(v >= 0)` folds NaN (and * negatives) to 0. +/-Infinity are handled by the two clamps. */ if (!(v >= 0)) v = 0; if (v > INT_MAX) v = INT_MAX; *out = (int)v; return true; } static bool json_bool(const char **p, bool *out) { json_ws(p); if (json_lit(p, "true")) { *out = true; return true; } if (json_lit(p, "false")) { *out = false; return true; } return false; } /* The request parser only understands the API fields we use and skips the * rest. Skipping is recursive because JSON values nest, so keep an explicit * ceiling: without it, a useless ignored field like {"x":[[[...]]]} can spend * the whole C stack before the request is rejected. */ #define JSON_MAX_NESTING 256 static bool json_skip_value_depth(const char **p, int depth); static bool json_skip_array_depth(const char **p, int depth) { if (depth >= JSON_MAX_NESTING) return false; json_ws(p); if (**p != '[') return false; (*p)++; json_ws(p); if (**p == ']') { (*p)++; return true; } for (;;) { if (!json_skip_value_depth(p, depth + 1)) return false; json_ws(p); if (**p == ']') { (*p)++; return true; } if (**p != ',') return false; (*p)++; } } static bool json_skip_object_depth(const char **p, int depth) { if (depth >= JSON_MAX_NESTING) return false; json_ws(p); if (**p != '{') return false; (*p)++; json_ws(p); if (**p == '}') { (*p)++; return true; } for (;;) { char *key = NULL; if (!json_string(p, &key)) return false; free(key); json_ws(p); if (**p != ':') return false; (*p)++; if (!json_skip_value_depth(p, depth + 1)) return false; json_ws(p); if (**p == '}') { (*p)++; return true; } if (**p != ',') return false; (*p)++; } } static bool json_skip_value_depth(const char **p, int depth) { json_ws(p); if (**p == '"') { char *s = NULL; bool ok = json_string(p, &s); free(s); return ok; } if (**p == '{') return json_skip_object_depth(p, depth); if (**p == '[') return json_skip_array_depth(p, depth); if (json_lit(p, "true") || json_lit(p, "false") || json_lit(p, "null")) return true; double v = 0.0; return json_number(p, &v); } static bool json_skip_value(const char **p) { return json_skip_value_depth(p, 0); } static bool json_raw_value(const char **p, char **out) { json_ws(p); const char *start = *p; if (!json_skip_value(p)) return false; size_t n = (size_t)(*p - start); char *s = xmalloc(n + 1); memcpy(s, start, n); s[n] = '\0'; *out = s; return true; } static char *json_minify_raw_value(const char *json) { const char *p = json ? json : "null"; json_ws(&p); const char *start = p; if (!json_skip_value(&p)) return xstrdup(json ? json : "null"); const char *end = p; buf b = {0}; bool in_string = false; bool escape = false; for (const char *s = start; s < end; s++) { unsigned char c = (unsigned char)*s; if (in_string) { buf_putc(&b, (char)c); if (escape) escape = false; else if (c == '\\') escape = true; else if (c == '"') in_string = false; } else if (c == '"') { in_string = true; buf_putc(&b, (char)c); } else if (!isspace(c)) { buf_putc(&b, (char)c); } } return buf_take(&b); } #define SERVER_IMAGE_MARKER_BYTES 64 typedef struct { char marker[SERVER_IMAGE_MARKER_BYTES]; uint8_t *encoded; size_t encoded_len; } server_image_input; typedef struct { server_image_input *v; size_t len; size_t cap; } server_image_inputs; static void server_image_inputs_free(server_image_inputs *images) { if (!images) return; for (size_t i = 0; i < images->len; i++) free(images->v[i].encoded); free(images->v); memset(images, 0, sizeof(*images)); } static int base64_value(unsigned char c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return 26 + c - 'a'; if (c >= '0' && c <= '9') return 52 + c - '0'; if (c == '+') return 62; if (c == '/') return 63; return -1; } static bool server_decode_base64(const char *src, uint8_t **out, size_t *out_len) { const size_t n = src ? strlen(src) : 0; if (n == 0 || (n & 3u) != 0 || n > 64u * 1024u * 1024u) return false; size_t cap = (n / 4u) * 3u; uint8_t *decoded = xmalloc(cap ? cap : 1); size_t used = 0; for (size_t i = 0; i < n; i += 4) { int a = base64_value((unsigned char)src[i]); int b = base64_value((unsigned char)src[i + 1]); bool pad2 = src[i + 2] == '='; bool pad3 = src[i + 3] == '='; int c = pad2 ? 0 : base64_value((unsigned char)src[i + 2]); int d = pad3 ? 0 : base64_value((unsigned char)src[i + 3]); if (a < 0 || b < 0 || c < 0 || d < 0 || (pad2 && !pad3) || ((pad2 || pad3) && i + 4 != n)) { free(decoded); return false; } uint32_t bits = ((uint32_t)a << 18) | ((uint32_t)b << 12) | ((uint32_t)c << 6) | (uint32_t)d; decoded[used++] = (uint8_t)(bits >> 16); if (!pad2) decoded[used++] = (uint8_t)(bits >> 8); if (!pad3) decoded[used++] = (uint8_t)bits; } *out = decoded; *out_len = used; return true; } static bool server_image_media_type(const char *media_type) { return media_type && (!strcasecmp(media_type, "image/png") || !strcasecmp(media_type, "image/jpeg") || !strcasecmp(media_type, "image/jpg")); } static bool server_image_inputs_push_base64(server_image_inputs *images, const char *media_type, const char *base64, char marker[SERVER_IMAGE_MARKER_BYTES]) { if (!server_image_media_type(media_type)) return false; server_image_input image = {0}; if (!server_decode_base64(base64, &image.encoded, &image.encoded_len)) return false; unsigned char nonce[12]; if (!random_bytes(nonce, sizeof(nonce))) { uint64_t fallback = (uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32) ^ (uint64_t)(uintptr_t)images; memcpy(nonce, &fallback, sizeof(fallback)); memset(nonce + sizeof(fallback), 0, sizeof(nonce) - sizeof(fallback)); } static const char hex[] = "0123456789abcdef"; size_t pos = (size_t)snprintf(image.marker, sizeof(image.marker), "\036" "DS4_IMAGE_"); for (size_t i = 0; i < sizeof(nonce) && pos + 2 < sizeof(image.marker); i++) { image.marker[pos++] = hex[nonce[i] >> 4]; image.marker[pos++] = hex[nonce[i] & 15]; } image.marker[pos++] = '\x1f'; image.marker[pos] = '\0'; if (images->len == images->cap) { size_t cap = images->cap ? images->cap * 2 : 2; images->v = xrealloc(images->v, cap * sizeof(images->v[0])); images->cap = cap; } images->v[images->len++] = image; snprintf(marker, SERVER_IMAGE_MARKER_BYTES, "%s", image.marker); return true; } static bool server_image_inputs_push_data_uri( server_image_inputs *images, const char *uri, char marker[SERVER_IMAGE_MARKER_BYTES]) { static const char png[] = "data:image/png;base64,"; static const char jpeg[] = "data:image/jpeg;base64,"; static const char jpg[] = "data:image/jpg;base64,"; if (!uri) return false; if (!strncmp(uri, png, sizeof(png) - 1)) return server_image_inputs_push_base64( images, "image/png", uri + sizeof(png) - 1, marker); if (!strncmp(uri, jpeg, sizeof(jpeg) - 1)) return server_image_inputs_push_base64( images, "image/jpeg", uri + sizeof(jpeg) - 1, marker); if (!strncmp(uri, jpg, sizeof(jpg) - 1)) return server_image_inputs_push_base64( images, "image/jpg", uri + sizeof(jpg) - 1, marker); return false; } static void append_owned_text(char **dst, const char *text) { buf b = {0}; buf_puts(&b, *dst ? *dst : ""); buf_puts(&b, text ? text : ""); free(*dst); *dst = buf_take(&b); } static bool json_content(const char **p, char **out) { json_ws(p); if (**p == '"') return json_string(p, out); if (json_lit(p, "null")) { *out = xstrdup(""); return true; } if (**p != '[') { if (!json_skip_value(p)) return false; *out = xstrdup(""); return true; } (*p)++; buf b = {0}; json_ws(p); while (**p && **p != ']') { if (**p == '"') { char *s = NULL; if (!json_string(p, &s)) goto fail; buf_puts(&b, s); free(s); } else if (**p == '{') { (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto fail; json_ws(p); if (**p != ':') { free(key); goto fail; } (*p)++; if (!strcmp(key, "text")) { char *s = NULL; if (!json_string(p, &s)) { free(key); goto fail; } buf_puts(&b, s); free(s); } else if (!json_skip_value(p)) { free(key); goto fail; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto fail; (*p)++; } else if (!json_skip_value(p)) { goto fail; } json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') goto fail; (*p)++; *out = buf_take(&b); return true; fail: buf_free(&b); return false; } static bool json_string_replace(const char **p, char **dst) { char *tmp = NULL; if (!json_string(p, &tmp)) return false; free(*dst); *dst = tmp; return true; } static bool json_raw_value_replace(const char **p, char **dst) { char *tmp = NULL; if (!json_raw_value(p, &tmp)) return false; free(*dst); *dst = tmp; return true; } static bool json_content_replace(const char **p, char **dst) { char *tmp = NULL; if (!json_content(p, &tmp)) return false; free(*dst); *dst = tmp; return true; } typedef enum { REQ_CHAT, REQ_COMPLETION, } req_kind; typedef enum { API_OPENAI, API_ANTHROPIC, API_RESPONSES, } api_style; typedef enum { SERVER_MODEL_SYNTAX_DEEPSEEK, SERVER_MODEL_SYNTAX_GLM, SERVER_MODEL_SYNTAX_DEEPSEEK41, SERVER_MODEL_SYNTAX_QWEN, } server_model_syntax; #define DS41_TOOL_CALLS_START "<|DSML| calls>" #define DS41_TOOL_CALLS_END "" #define DS41_INVOKE_START "<|DSML| invoke" #define DS41_INVOKE_END "" #define DS41_PARAM_START "<|DSML| parameter" #define DS41_PARAM_END "" static void random_tool_id(char *dst, size_t dstlen, api_style api) { static uint64_t fallback_ctr; unsigned char bytes[16]; const char *prefix = api == API_ANTHROPIC ? "toolu_" : "call_"; size_t pos = snprintf(dst, dstlen, "%s", prefix); if (pos >= dstlen) return; if (!random_bytes(bytes, sizeof(bytes))) { uint64_t a = ((uint64_t)time(NULL) << 32) ^ (uint64_t)getpid(); uint64_t b = ++fallback_ctr ^ (uint64_t)(uintptr_t)dst; memcpy(bytes, &a, sizeof(a)); memcpy(bytes + sizeof(a), &b, sizeof(b)); } static const char hex[] = "0123456789abcdef"; for (size_t i = 0; i < sizeof(bytes) && pos + 2 < dstlen; i++) { dst[pos++] = hex[bytes[i] >> 4]; dst[pos++] = hex[bytes[i] & 15]; } dst[pos] = '\0'; } typedef struct server server; static void server_inference_lock(server *s); static void server_inference_unlock(server *s); static bool server_encode_image(server *s, const server_image_input *input, ds4_vision_embedding *out, char *err, size_t errlen); typedef struct { char *id; char *name; char *arguments; } tool_call; typedef struct { tool_call *v; int len; int cap; char *raw_tool_text; } tool_calls; typedef struct { int mem; int disk; int canonical; int missing_ids; } tool_replay_stats; typedef struct { char *name; char *wire_name; char *namespace; /* Distinguish the Responses hosted tool from a normal function that * happens to be named "tool_search". */ bool responses_tool_search; char **prop; char **prop_schema; int len; int cap; } tool_schema_order; typedef struct { tool_schema_order *v; int len; int cap; } tool_schema_orders; typedef struct { size_t begin, end; char *id, *content; } tool_result_span; typedef struct { char *role; char *content; server_image_inputs images; char *reasoning; char *tool_call_id; char **tool_call_ids; int tool_call_ids_len; int tool_call_ids_cap; tool_calls calls; tool_result_span *tool_results; int tool_results_len; } chat_msg; typedef struct { chat_msg *v; int len; int cap; } chat_msgs; static void tool_memory_attach_to_messages(server *s, chat_msgs *msgs, tool_replay_stats *stats); static bool tool_memory_has_id(server *s, const char *id); static void kv_cache_restore_tool_memory_for_messages(server *s, const chat_msgs *msgs); typedef struct { char **v; int len; int cap; size_t max_len; } stop_list; static void stop_list_clear(stop_list *stops); static bool id_list_contains(const stop_list *ids, const char *id); static void id_list_push_unique(stop_list *ids, const char *id); static void id_list_free(stop_list *ids); static bool responses_live_has_call_id(server *s, const char *id); static bool anthropic_live_has_call_id(server *s, const char *id); static stop_list live_tool_call_order(server *s, api_style api, const stop_list *ids); typedef struct { req_kind kind; api_style api; server_model_syntax model_syntax; ds4_tokens prompt; ds4_vision_span *images; char (*image_markers)[SERVER_IMAGE_MARKER_BYTES]; size_t image_count; char *model; bool model_from_request; stop_list stops; char *raw_body; char *prompt_text; tool_schema_orders tool_orders; int max_tokens; int top_k; float temperature; float top_p; float min_p; /* Explicit client sampling wins even in thinking mode; the fixed * DeepSeek-style thinking defaults apply only to omitted knobs. */ bool temperature_set; bool top_p_set; bool min_p_set; bool top_k_set; uint64_t seed; bool stream; bool stream_include_usage; bool ignore_eos; int cache_read_tokens; int cache_write_tokens; ds4_think_mode think_mode; bool has_tools; bool prompt_preserves_reasoning; /* For /v1/responses: emit reasoning_summary_* events / fields only when the * client opted in via reasoning.summary. Other APIs leave this false; the * field is ignored on those code paths. */ bool reasoning_summary_emit; /* Responses continuation contract: * * A live Responses tool loop is not a normal "new prompt with a long * prefix" request. The protocol gives tool outputs a call_id that binds * them to a prior assistant tool call. If that call_id is still known in * memory, the live KV is the authoritative prefix, including any hidden * thinking that the client did not replay. These fields carry the parsed * evidence needed by generate_job() to append only the new suffix. * * A tool-output-only request has no stateless prefix to match. If the live * call_id binding is gone by the time the worker executes it, DS4 must ask * for a full replay rather than cold-prefilling a prompt that starts with a * naked tool result. Similarly, if live state is gone, a reasoning-mode * tool replay must contain the prior reasoning item (or an equivalent * opaque reasoning state from a future implementation). */ bool responses_requires_live_tool_state; bool responses_requires_live_reasoning; stop_list responses_live_call_ids; char *responses_live_suffix_text; bool anthropic_requires_live_tool_state; stop_list anthropic_live_call_ids; char *anthropic_live_suffix_text; tool_replay_stats tool_replay; } request; static void tool_call_free(tool_call *tc) { free(tc->id); free(tc->name); free(tc->arguments); memset(tc, 0, sizeof(*tc)); } static void tool_calls_free(tool_calls *calls) { for (int i = 0; i < calls->len; i++) tool_call_free(&calls->v[i]); free(calls->raw_tool_text); free(calls->v); memset(calls, 0, sizeof(*calls)); } static void tool_calls_push(tool_calls *calls, tool_call tc) { if (calls->len == calls->cap) { calls->cap = calls->cap ? calls->cap * 2 : 4; calls->v = xrealloc(calls->v, (size_t)calls->cap * sizeof(calls->v[0])); } calls->v[calls->len++] = tc; } static void chat_msg_add_tool_call_id(chat_msg *m, const char *id) { if (!m || !id || !id[0]) return; if (!m->tool_call_id) m->tool_call_id = xstrdup(id); for (int i = 0; i < m->tool_call_ids_len; i++) { if (m->tool_call_ids[i] && !strcmp(m->tool_call_ids[i], id)) return; } if (m->tool_call_ids_len == m->tool_call_ids_cap) { m->tool_call_ids_cap = m->tool_call_ids_cap ? m->tool_call_ids_cap * 2 : 2; m->tool_call_ids = xrealloc(m->tool_call_ids, (size_t)m->tool_call_ids_cap * sizeof(m->tool_call_ids[0])); } m->tool_call_ids[m->tool_call_ids_len++] = xstrdup(id); } static void chat_msg_free(chat_msg *m) { free(m->role); free(m->content); server_image_inputs_free(&m->images); free(m->reasoning); free(m->tool_call_id); for (int i = 0; i < m->tool_call_ids_len; i++) free(m->tool_call_ids[i]); free(m->tool_call_ids); tool_calls_free(&m->calls); for (int i = 0; i < m->tool_results_len; i++) { free(m->tool_results[i].id); free(m->tool_results[i].content); } free(m->tool_results); memset(m, 0, sizeof(*m)); } static void chat_msgs_free(chat_msgs *msgs) { for (int i = 0; i < msgs->len; i++) chat_msg_free(&msgs->v[i]); free(msgs->v); memset(msgs, 0, sizeof(*msgs)); } static void chat_msgs_push(chat_msgs *msgs, chat_msg msg) { if (msgs->len == msgs->cap) { msgs->cap = msgs->cap ? msgs->cap * 2 : 8; msgs->v = xrealloc(msgs->v, (size_t)msgs->cap * sizeof(msgs->v[0])); } msgs->v[msgs->len++] = msg; } static void tool_schema_order_free(tool_schema_order *o) { free(o->name); free(o->wire_name); free(o->namespace); for (int i = 0; i < o->len; i++) { free(o->prop[i]); free(o->prop_schema[i]); } free(o->prop); free(o->prop_schema); memset(o, 0, sizeof(*o)); } static void tool_schema_orders_free(tool_schema_orders *orders) { for (int i = 0; i < orders->len; i++) tool_schema_order_free(&orders->v[i]); free(orders->v); memset(orders, 0, sizeof(*orders)); } static void tool_schema_order_prop_push(tool_schema_order *o, char *prop, char *schema) { if (o->len == o->cap) { o->cap = o->cap ? o->cap * 2 : 8; o->prop = xrealloc(o->prop, (size_t)o->cap * sizeof(o->prop[0])); o->prop_schema = xrealloc(o->prop_schema, (size_t)o->cap * sizeof(o->prop_schema[0])); } o->prop_schema[o->len] = schema; o->prop[o->len++] = prop; } static int tool_schema_orders_find_index(const tool_schema_orders *orders, const char *name) { if (!orders || !name) return -1; for (int i = 0; i < orders->len; i++) { if (orders->v[i].name && !strcmp(orders->v[i].name, name)) return i; } return -1; } static void tool_schema_orders_push(tool_schema_orders *orders, tool_schema_order order) { int idx = tool_schema_orders_find_index(orders, order.name); if (idx >= 0) { tool_schema_order_free(&orders->v[idx]); orders->v[idx] = order; return; } if (orders->len == orders->cap) { orders->cap = orders->cap ? orders->cap * 2 : 8; orders->v = xrealloc(orders->v, (size_t)orders->cap * sizeof(orders->v[0])); } orders->v[orders->len++] = order; } static const tool_schema_order *tool_schema_orders_find(const tool_schema_orders *orders, const char *name) { int idx = tool_schema_orders_find_index(orders, name); return idx >= 0 ? &orders->v[idx] : NULL; } static void request_init(request *r, req_kind kind, int max_tokens) { memset(r, 0, sizeof(*r)); r->kind = kind; r->api = API_OPENAI; r->model_syntax = SERVER_MODEL_SYNTAX_DEEPSEEK; r->model = xstrdup("deepseek-v4-flash"); r->max_tokens = max_tokens; r->top_k = 0; r->temperature = DS4_DEFAULT_TEMPERATURE; r->top_p = DS4_DEFAULT_TOP_P; r->min_p = DS4_DEFAULT_MIN_P; r->think_mode = DS4_THINK_HIGH; } static bool parse_ignore_eos_value(const char **p, request *r) { return p && r && json_bool(p, &r->ignore_eos); } static bool request_validate_ignore_eos(const request *r, char *err, size_t errlen) { if (!r || !r->ignore_eos) return true; if (r->temperature_set && r->temperature == 0.0f) return true; snprintf(err, errlen, "ignore_eos requires an explicit temperature of 0"); return false; } static void request_free(request *r) { ds4_tokens_free(&r->prompt); for (size_t i = 0; i < r->image_count; i++) ds4_vision_embedding_free(&r->images[i].embedding); free(r->images); free(r->image_markers); free(r->model); for (int i = 0; i < r->stops.len; i++) free(r->stops.v[i]); free(r->stops.v); free(r->raw_body); free(r->prompt_text); stop_list_clear(&r->responses_live_call_ids); free(r->responses_live_call_ids.v); free(r->responses_live_suffix_text); stop_list_clear(&r->anthropic_live_call_ids); free(r->anthropic_live_call_ids.v); free(r->anthropic_live_suffix_text); tool_schema_orders_free(&r->tool_orders); memset(r, 0, sizeof(*r)); } static ds4_think_mode think_mode_from_enabled(bool enabled, ds4_think_mode effort) { if (!enabled || effort == DS4_THINK_NONE) return DS4_THINK_NONE; return effort; } static bool parse_reasoning_effort_name(const char *s, ds4_think_mode *out) { if (!s) return false; if (!strcmp(s, "max")) { *out = DS4_THINK_MAX; return true; } if (!strcmp(s, "xhigh") || !strcmp(s, "high")) { *out = DS4_THINK_HIGH; return true; } if (!strcmp(s, "medium")) { *out = DS4_THINK_MEDIUM; return true; } if (!strcmp(s, "low") || !strcmp(s, "minimal")) { /* "minimal" is the smallest non-zero level; callers that need *no* * reasoning must use "none" instead. */ *out = DS4_THINK_LOW; return true; } if (!strcmp(s, "none")) { *out = DS4_THINK_NONE; return true; } return false; } static bool parse_reasoning_effort_value(const char **p, ds4_think_mode *out) { json_ws(p); if (json_lit(p, "null")) return true; char *effort = NULL; if (!json_string(p, &effort)) return false; bool ok = parse_reasoning_effort_name(effort, out) || ds4_think_mode_parse_level(effort, out); free(effort); return ok; } static bool parse_thinking_control_value(const char **p, bool *thinking_enabled) { json_ws(p); if (json_lit(p, "null")) return true; if (**p == 't' || **p == 'f') return json_bool(p, thinking_enabled); if (**p != '{') return json_skip_value(p); (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; if (!strcmp(key, "type")) { char *type = NULL; if (!json_string(p, &type)) { free(key); return false; } if (!strcmp(type, "enabled")) *thinking_enabled = true; else if (!strcmp(type, "disabled")) *thinking_enabled = false; free(type); } else if (!json_skip_value(p)) { free(key); return false; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } /* chat_template_kwargs as the Qwen3.8 model card documents them: enable_thinking * and reasoning_effort are applied, other keys are ignored */ static bool parse_chat_template_kwargs(const char **p, bool *thinking_enabled, bool *got_thinking, ds4_think_mode *effort) { json_ws(p); if (json_lit(p, "null")) return true; if (**p != '{') return json_skip_value(p); (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; json_ws(p); bool ok; if (!strcmp(key, "enable_thinking")) { ok = json_bool(p, thinking_enabled); if (ok) *got_thinking = true; } else if (!strcmp(key, "reasoning_effort")) { ok = parse_reasoning_effort_value(p, effort); } else { ok = json_skip_value(p); } free(key); if (!ok) return false; json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } static bool parse_output_config_effort(const char **p, ds4_think_mode *effort) { json_ws(p); if (json_lit(p, "null")) return true; if (**p != '{') return json_skip_value(p); (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; if (!strcmp(key, "effort")) { if (!parse_reasoning_effort_value(p, effort)) { free(key); return false; } } else if (!json_skip_value(p)) { free(key); return false; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } static bool model_alias_disables_thinking(const char *model) { return model && (!strcmp(model, "deepseek-chat") || !strcmp(model, "qwen3.8-flash-next-chat") || !strcmp(model, "qwen3.8-flash-next-no-think") || !strcmp(model, "qwen3.8-flash-next-nothink") || !strcmp(model, "qwen/qwen3.8-flash-next-chat") || !strcmp(model, "glm-5.2-chat") || !strcmp(model, "glm-5.2-no-think") || !strcmp(model, "glm-5.2-nothink") || !strcmp(model, "zai/glm-5.2-chat") || !strcmp(model, "glm-5.3-flash-chat") || !strcmp(model, "glm-5.3-flash-no-think") || !strcmp(model, "glm-5.3-flash-nothink") || !strcmp(model, "zai/glm-5.3-flash-chat")); } static bool model_alias_enables_thinking(const char *model) { return model && (!strcmp(model, "deepseek-reasoner") || !strcmp(model, "qwen3.8-flash-next-reasoner") || !strcmp(model, "qwen/qwen3.8-flash-next-reasoner") || !strcmp(model, "glm-5.2-reasoner") || !strcmp(model, "zai/glm-5.2-reasoner") || !strcmp(model, "glm-5.3-flash-reasoner") || !strcmp(model, "zai/glm-5.3-flash-reasoner")); } static server_model_syntax server_model_syntax_for_engine(ds4_engine *engine) { if (ds4_engine_is_qwen4(engine)) return SERVER_MODEL_SYNTAX_QWEN; return ds4_engine_is_glm_dsa(engine) ? SERVER_MODEL_SYNTAX_GLM : ds4_engine_is_deepseek41(engine) ? SERVER_MODEL_SYNTAX_DEEPSEEK41 : SERVER_MODEL_SYNTAX_DEEPSEEK; } static const char *server_model_id_from_engine(ds4_engine *engine) { if (ds4_engine_is_deepseek41(engine)) return "deepseek-v4.1-flash"; if (ds4_engine_is_qwen4(engine)) return "qwen3.8-flash-next"; if (ds4_engine_is_glm53(engine)) return "glm-5.3-flash"; if (ds4_engine_is_glm_dsa(engine)) return "glm-5.2"; return ds4_engine_model_id(engine) == 1 ? "deepseek-v4-pro" : "deepseek-v4-flash"; } static bool server_model_alias_known(const char *id) { return id && (!strcmp(id, "deepseek-v4-flash") || !strcmp(id, "deepseek-v4.1-flash") || !strcmp(id, "qwen3.8-flash-next") || !strcmp(id, "qwen3.8-flash-next-chat") || !strcmp(id, "qwen3.8-flash-next-no-think") || !strcmp(id, "qwen3.8-flash-next-nothink") || !strcmp(id, "qwen3.8-flash-next-reasoner") || !strcmp(id, "qwen/qwen3.8-flash-next") || !strcmp(id, "qwen/qwen3.8-flash-next-chat") || !strcmp(id, "qwen/qwen3.8-flash-next-reasoner") || !strcmp(id, "deepseek-v4-pro") || !strcmp(id, "glm-5.2") || !strcmp(id, "glm-5.2-chat") || !strcmp(id, "glm-5.2-no-think") || !strcmp(id, "glm-5.2-nothink") || !strcmp(id, "glm-5.2-reasoner") || !strcmp(id, "zai/glm-5.2") || !strcmp(id, "zai/glm-5.2-chat") || !strcmp(id, "zai/glm-5.2-reasoner") || !strcmp(id, "glm-5.3-flash") || !strcmp(id, "glm-5.3-flash-chat") || !strcmp(id, "glm-5.3-flash-no-think") || !strcmp(id, "glm-5.3-flash-nothink") || !strcmp(id, "glm-5.3-flash-reasoner") || !strcmp(id, "zai/glm-5.3-flash") || !strcmp(id, "zai/glm-5.3-flash-chat") || !strcmp(id, "zai/glm-5.3-flash-reasoner")); } static void stop_list_clear(stop_list *stops) { for (int i = 0; i < stops->len; i++) free(stops->v[i]); stops->len = 0; stops->max_len = 0; } static void stop_list_push(stop_list *stops, char *s) { if (!s || !s[0]) { free(s); return; } if (stops->len == stops->cap) { stops->cap = stops->cap ? stops->cap * 2 : 4; stops->v = xrealloc(stops->v, (size_t)stops->cap * sizeof(stops->v[0])); } size_t n = strlen(s); if (n > stops->max_len) stops->max_len = n; stops->v[stops->len++] = s; } static bool parse_stop(const char **p, stop_list *out) { json_ws(p); stop_list_clear(out); if (**p == '"') { char *s = NULL; if (!json_string(p, &s)) return false; stop_list_push(out, s); return true; } if (**p != '[') return json_skip_value(p); (*p)++; json_ws(p); while (**p && **p != ']') { if (**p == '"') { char *s = NULL; if (!json_string(p, &s)) return false; stop_list_push(out, s); } else if (!json_skip_value(p)) { return false; } json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') return false; (*p)++; return true; } static bool stop_list_find_from(const stop_list *stops, const char *text, size_t from, size_t *pos, size_t *len) { if (!stops->len || !text) return false; bool found = false; size_t best_pos = 0, best_len = 0; for (int i = 0; i < stops->len; i++) { const char *p = strstr(text + from, stops->v[i]); if (!p) continue; size_t ppos = (size_t)(p - text); size_t plen = strlen(stops->v[i]); if (!found || ppos < best_pos) { found = true; best_pos = ppos; best_len = plen; } } if (!found) return false; *pos = best_pos; *len = best_len; return true; } static size_t stop_list_stream_safe_len(const stop_list *stops, size_t text_len) { /* Streaming cannot emit the last max_stop_len-1 bytes yet: a stop sequence * may start there and finish in the next token. The final flush releases * this small tail once generation ends without a stop hit. */ if (!stops->len || stops->max_len <= 1) return text_len; const size_t hold = stops->max_len - 1; return text_len > hold ? text_len - hold : 0; } static int utf8_expected_len(unsigned char c) { if (c < 0x80) return 1; if (c >= 0xc2 && c <= 0xdf) return 2; if (c >= 0xe0 && c <= 0xef) return 3; if (c >= 0xf0 && c <= 0xf4) return 4; return 1; } /* Tokenizers can split a multi-byte UTF-8 character across two tokens. If an * SSE delta ends at that boundary, some clients replace the incomplete byte * sequence with U+FFFD and later send the corrupted text back, destroying KV * cache prefix matches. Hold only the trailing incomplete character; the next * generated token will complete it. */ static size_t utf8_stream_safe_len(const char *s, size_t start, size_t limit, bool final) { if (final || !s || limit <= start) return limit; size_t p = limit; int cont = 0; while (p > start && cont < 4 && (((unsigned char)s[p - 1] & 0xc0) == 0x80)) { p--; cont++; } if (p == limit) { return utf8_expected_len((unsigned char)s[limit - 1]) > 1 ? limit - 1 : limit; } if (p == start && (((unsigned char)s[p] & 0xc0) == 0x80)) return start; size_t lead = p - 1; int need = utf8_expected_len((unsigned char)s[lead]); return (limit - lead) < (size_t)need ? lead : limit; } static bool parse_stream_options(const char **p, bool *include_usage) { json_ws(p); if (**p != '{') return json_skip_value(p); (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; if (!strcmp(key, "include_usage")) { if (!json_bool(p, include_usage)) { free(key); return false; } } else if (!json_skip_value(p)) { free(key); return false; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } static bool parse_function_call(const char **p, tool_call *tc) { json_ws(p); if (**p != '{') return false; (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto bad; json_ws(p); if (**p != ':') { free(key); goto bad; } (*p)++; if (!strcmp(key, "name")) { if (!json_string_replace(p, &tc->name)) { free(key); goto bad; } } else if (!strcmp(key, "arguments")) { json_ws(p); if (**p == '"') { if (!json_string_replace(p, &tc->arguments)) { free(key); goto bad; } } else if (!json_raw_value_replace(p, &tc->arguments)) { free(key); goto bad; } } else if (!json_skip_value(p)) { free(key); goto bad; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto bad; (*p)++; return true; bad: return false; } static bool parse_tool_calls_value(const char **p, tool_calls *calls) { json_ws(p); if (json_lit(p, "null")) return true; if (**p != '[') return false; (*p)++; json_ws(p); while (**p && **p != ']') { if (**p != '{') return false; (*p)++; tool_call tc = {0}; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto bad; json_ws(p); if (**p != ':') { free(key); goto bad; } (*p)++; if (!strcmp(key, "id")) { if (!json_string_replace(p, &tc.id)) { free(key); goto bad; } } else if (!strcmp(key, "function")) { if (!parse_function_call(p, &tc)) { free(key); goto bad; } } else if (!json_skip_value(p)) { free(key); goto bad; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto bad; (*p)++; if (tc.name && tc.arguments) { tool_calls_push(calls, tc); memset(&tc, 0, sizeof(tc)); } tool_call_free(&tc); json_ws(p); if (**p == ',') (*p)++; json_ws(p); continue; bad: tool_call_free(&tc); return false; } if (**p != ']') return false; (*p)++; return true; } static void append_raw_json_line(buf *b, const char *json) { if (!json || !json[0]) return; if (b->len) buf_putc(b, '\n'); buf_puts(b, json); } static void json_escape(buf *b, const char *s); static char *openai_function_schema_from_tool(const char *raw) { const char *p = raw; json_ws(&p); if (*p != '{') return NULL; p++; json_ws(&p); while (*p && *p != '}') { char *key = NULL; char *value = NULL; if (!json_string(&p, &key)) return NULL; json_ws(&p); if (*p != ':') { free(key); return NULL; } p++; if (!strcmp(key, "function")) { free(key); if (!json_raw_value(&p, &value)) return NULL; return value; } free(key); if (!json_skip_value(&p)) return NULL; json_ws(&p); if (*p == ',') p++; json_ws(&p); } return NULL; } static char *responses_special_schema_from_tool(const char *raw) { const char *p = raw; json_ws(&p); if (*p != '{') return NULL; p++; char *type = NULL; char *description = NULL; char *parameters = NULL; char *out = NULL; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto done; json_ws(&p); if (*p != ':') { free(key); goto done; } p++; if (!strcmp(key, "type")) { if (!json_string_replace(&p, &type)) { free(key); goto done; } } else if (!strcmp(key, "description")) { if (!json_string_replace(&p, &description)) { free(key); goto done; } } else if (!strcmp(key, "parameters")) { if (!json_raw_value_replace(&p, ¶meters)) { free(key); goto done; } } else if (!json_skip_value(&p)) { free(key); goto done; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (type && !strcmp(type, "tool_search")) { buf b = {0}; buf_puts(&b, "{\"name\":\"tool_search\",\"description\":"); json_escape(&b, description ? description : "Search available tools."); buf_puts(&b, ",\"parameters\":"); buf_puts(&b, parameters ? parameters : "{\"type\":\"object\",\"properties\":{}}"); buf_putc(&b, '}'); out = buf_take(&b); } done: free(type); free(description); free(parameters); return out; } static char *responses_namespace_function_schema_from_tool(const char *raw, const char *namespace, char **wire_name) { const char *p = raw; json_ws(&p); if (*p != '{') return NULL; p++; char *type = NULL; char *name = NULL; char *description = NULL; char *parameters = NULL; char *out = NULL; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto done; json_ws(&p); if (*p != ':') { free(key); goto done; } p++; if (!strcmp(key, "type")) { if (!json_string_replace(&p, &type)) { free(key); goto done; } } else if (!strcmp(key, "name")) { if (!json_string_replace(&p, &name)) { free(key); goto done; } } else if (!strcmp(key, "description")) { if (!json_string_replace(&p, &description)) { free(key); goto done; } } else if (!strcmp(key, "parameters") || !strcmp(key, "input_schema")) { if (!json_raw_value_replace(&p, ¶meters)) { free(key); goto done; } } else if (!json_skip_value(&p)) { free(key); goto done; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if ((!type || !strcmp(type, "function")) && namespace && name && name[0]) { buf prompt_name = {0}; buf_puts(&prompt_name, namespace); buf_puts(&prompt_name, name); buf b = {0}; buf_puts(&b, "{\"name\":"); json_escape(&b, prompt_name.ptr ? prompt_name.ptr : name); buf_puts(&b, ",\"description\":"); json_escape(&b, description ? description : ""); buf_puts(&b, ",\"parameters\":"); buf_puts(&b, parameters ? parameters : "{\"type\":\"object\",\"properties\":{}}"); buf_putc(&b, '}'); out = buf_take(&b); if (wire_name) *wire_name = xstrdup(name); buf_free(&prompt_name); } done: free(type); free(name); free(description); free(parameters); return out; } static bool parse_schema_properties(const char *json, tool_schema_order *order) { const char *p = json; json_ws(&p); if (*p != '{') return false; p++; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) return false; json_ws(&p); if (*p != ':') { free(key); return false; } p++; if (!strcmp(key, "properties")) { free(key); json_ws(&p); if (*p != '{') return false; p++; json_ws(&p); while (*p && *p != '}') { char *prop = NULL; if (!json_string(&p, &prop)) return false; json_ws(&p); if (*p != ':') { free(prop); return false; } p++; char *schema = NULL; if (!json_raw_value(&p, &schema)) { free(prop); return false; } tool_schema_order_prop_push(order, prop, schema); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (*p != '}') return false; p++; } else { free(key); if (!json_skip_value(&p)) return false; } json_ws(&p); if (*p == ',') p++; json_ws(&p); } return *p == '}'; } static void tool_schema_orders_add_json_wire(tool_schema_orders *orders, const char *json, const char *namespace, const char *wire_name, bool responses_tool_search) { if (!orders || !json) return; const char *p = json; json_ws(&p); if (*p != '{') return; p++; tool_schema_order order = {0}; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto done; json_ws(&p); if (*p != ':') { free(key); goto done; } p++; if (!strcmp(key, "name")) { if (!json_string_replace(&p, &order.name)) { free(key); goto done; } } else if (!strcmp(key, "input_schema") || !strcmp(key, "parameters")) { char *schema = NULL; if (!json_raw_value(&p, &schema)) { free(key); goto done; } parse_schema_properties(schema, &order); free(schema); } else if (!json_skip_value(&p)) { free(key); goto done; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (order.name) { if (namespace && namespace[0]) order.namespace = xstrdup(namespace); if (wire_name && wire_name[0]) order.wire_name = xstrdup(wire_name); order.responses_tool_search = responses_tool_search; tool_schema_orders_push(orders, order); memset(&order, 0, sizeof(order)); } done: tool_schema_order_free(&order); } static void tool_schema_orders_add_json(tool_schema_orders *orders, const char *json) { tool_schema_orders_add_json_wire(orders, json, NULL, NULL, false); } static bool append_responses_namespace_tool_schemas(buf *schemas, tool_schema_orders *orders, const char *raw) { const char *p = raw; json_ws(&p); if (*p != '{') return false; p++; char *type = NULL; char *name = NULL; char *tools = NULL; bool appended = false; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto done; json_ws(&p); if (*p != ':') { free(key); goto done; } p++; if (!strcmp(key, "type")) { if (!json_string_replace(&p, &type)) { free(key); goto done; } } else if (!strcmp(key, "name")) { if (!json_string_replace(&p, &name)) { free(key); goto done; } } else if (!strcmp(key, "tools")) { if (!json_raw_value_replace(&p, &tools)) { free(key); goto done; } } else if (!json_skip_value(&p)) { free(key); goto done; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (!type || strcmp(type, "namespace") || !name || !tools) goto done; const char *tp = tools; json_ws(&tp); if (*tp != '[') goto done; tp++; json_ws(&tp); while (*tp && *tp != ']') { char *tool_raw = NULL; if (!json_raw_value(&tp, &tool_raw)) goto done; char *wire_name = NULL; char *schema = responses_namespace_function_schema_from_tool(tool_raw, name, &wire_name); if (schema) { append_raw_json_line(schemas, schema); tool_schema_orders_add_json_wire(orders, schema, name, wire_name, false); appended = true; } free(schema); free(wire_name); free(tool_raw); json_ws(&tp); if (*tp == ',') tp++; json_ws(&tp); } done: free(type); free(name); free(tools); return appended; } /* OpenAI wraps tools as {"type":"function","function":{...}}. Anthropic sends * the function schema directly as {"name":...,"input_schema":...}. The DS4 * prompt wants one raw function schema per line, so unwrap OpenAI tools and keep * already-direct schemas unchanged. Responses can additionally group tools in a * namespace item; those are flattened for DSML prompt rendering while preserving * their client-facing name and namespace for response output. */ static bool parse_tools_value(const char **p, char **out, tool_schema_orders *orders) { json_ws(p); if (json_lit(p, "null")) { *out = xstrdup(""); return true; } if (**p != '[') return false; (*p)++; buf schemas = {0}; json_ws(p); while (**p && **p != ']') { char *raw = NULL; if (!json_raw_value(p, &raw)) goto bad; char *function = openai_function_schema_from_tool(raw); if (function) { append_raw_json_line(&schemas, function); tool_schema_orders_add_json(orders, function); } else if (!append_responses_namespace_tool_schemas(&schemas, orders, raw)) { char *special = responses_special_schema_from_tool(raw); if (special) { append_raw_json_line(&schemas, special); tool_schema_orders_add_json_wire(orders, special, NULL, NULL, true); } else { append_raw_json_line(&schemas, raw); tool_schema_orders_add_json(orders, raw); } free(special); } free(function); free(raw); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') goto bad; (*p)++; *out = buf_take(&schemas); return true; bad: buf_free(&schemas); return false; } static bool parse_image_url_value(const char **p, char **url) { json_ws(p); if (**p == '"') return json_string(p, url); if (**p != '{') return false; (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; bool ok = true; if (!strcmp(key, "url")) { char *next = NULL; ok = json_string(p, &next); if (ok) { free(*url); *url = next; } } else { ok = json_skip_value(p); } free(key); if (!ok) return false; json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return *url != NULL; } static bool parse_openai_content_object(const char **p, chat_msg *msg) { if (**p != '{') return false; (*p)++; char *type = NULL; char *text = NULL; char *image_url = NULL; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto bad; json_ws(p); if (**p != ':') { free(key); goto bad; } (*p)++; bool ok = true; if (!strcmp(key, "type")) ok = json_string_replace(p, &type); else if (!strcmp(key, "text")) ok = json_content_replace(p, &text); else if (!strcmp(key, "image_url")) { free(image_url); image_url = NULL; ok = parse_image_url_value(p, &image_url); } else ok = json_skip_value(p); free(key); if (!ok) goto bad; json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto bad; (*p)++; if (type && (!strcmp(type, "image_url") || !strcmp(type, "input_image"))) { char marker[SERVER_IMAGE_MARKER_BYTES]; if (!server_image_inputs_push_data_uri(&msg->images, image_url, marker)) goto bad; append_owned_text(&msg->content, marker); } else if (text) { append_owned_text(&msg->content, text); } free(type); free(text); free(image_url); return true; bad: free(type); free(text); free(image_url); return false; } static bool parse_openai_content(const char **p, chat_msg *msg) { json_ws(p); if (**p == '"') { char *text = NULL; if (!json_string(p, &text)) return false; append_owned_text(&msg->content, text); free(text); return true; } if (json_lit(p, "null")) return true; if (**p != '[') return false; (*p)++; json_ws(p); while (**p && **p != ']') { if (**p == '"') { char *text = NULL; if (!json_string(p, &text)) return false; append_owned_text(&msg->content, text); free(text); } else if (**p == '{') { if (!parse_openai_content_object(p, msg)) return false; } else if (!json_skip_value(p)) { return false; } json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') return false; (*p)++; return true; } static bool parse_messages(const char **p, chat_msgs *msgs) { json_ws(p); if (**p != '[') return false; (*p)++; json_ws(p); while (**p && **p != ']') { if (**p != '{') return false; (*p)++; chat_msg msg = {0}; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto fail; json_ws(p); if (**p != ':') { free(key); goto fail; } (*p)++; if (!strcmp(key, "role")) { if (!json_string_replace(p, &msg.role)) { free(key); goto fail; } } else if (!strcmp(key, "content")) { free(msg.content); msg.content = NULL; server_image_inputs_free(&msg.images); if (!parse_openai_content(p, &msg)) { free(key); goto fail; } } else if (!strcmp(key, "reasoning_content")) { if (!json_content_replace(p, &msg.reasoning)) { free(key); goto fail; } } else if (!strcmp(key, "tool_call_id")) { char *id = NULL; if (!json_string(p, &id)) { free(key); goto fail; } chat_msg_add_tool_call_id(&msg, id); free(id); } else if (!strcmp(key, "tool_calls")) { tool_calls_free(&msg.calls); if (!parse_tool_calls_value(p, &msg.calls)) { free(key); goto fail; } } else if (!json_skip_value(p)) { free(key); goto fail; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto fail; (*p)++; if (!msg.role) msg.role = xstrdup("user"); if (!msg.content) msg.content = xstrdup(""); if (msg.images.len && strcmp(msg.role, "user")) goto fail; chat_msgs_push(msgs, msg); memset(&msg, 0, sizeof(msg)); json_ws(p); if (**p == ',') (*p)++; json_ws(p); continue; fail: chat_msg_free(&msg); return false; } if (**p != ']') return false; (*p)++; return true; } static void append_tool_result_text(buf *b, const char *s); static bool append_anthropic_block_content(buf *dst, const char *text) { if (!text || !text[0]) return true; buf_puts(dst, text); return true; } static bool parse_anthropic_image_source(const char **p, char **source_type, char **media_type, char **data) { json_ws(p); if (**p != '{') return false; (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; bool ok; if (!strcmp(key, "type")) ok = json_string_replace(p, source_type); else if (!strcmp(key, "media_type")) ok = json_string_replace(p, media_type); else if (!strcmp(key, "data")) ok = json_string_replace(p, data); else ok = json_skip_value(p); free(key); if (!ok) return false; json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } static bool parse_anthropic_content(const char **p, chat_msg *msg, bool allow_tools); /* Anthropic content is block-structured, while the engine consumes one compact * chat_msg per role. Parsing collapses text/thinking into strings, converts * assistant tool_use blocks to tool_calls, and keeps tool_result blocks as * escaped text because DS4 sees tool results in its chat template. */ static bool parse_anthropic_content_block(const char **p, bool allow_tools, chat_msg *msg) { if (**p != '{') return false; (*p)++; char *type = NULL; char *text = NULL; char *thinking = NULL; char *id = NULL; char *name = NULL; char *input = NULL; char *tool_result = NULL; char *source_type = NULL; char *media_type = NULL; char *image_data = NULL; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto bad; json_ws(p); if (**p != ':') { free(key); goto bad; } (*p)++; if (!strcmp(key, "type")) { if (!json_string_replace(p, &type)) { free(key); goto bad; } } else if (!strcmp(key, "text")) { if (!json_content_replace(p, &text)) { free(key); goto bad; } } else if (!strcmp(key, "thinking")) { if (!json_content_replace(p, &thinking)) { free(key); goto bad; } } else if (!strcmp(key, "id") || !strcmp(key, "tool_use_id")) { if (!json_string_replace(p, &id)) { free(key); goto bad; } } else if (!strcmp(key, "name")) { if (!json_string_replace(p, &name)) { free(key); goto bad; } } else if (!strcmp(key, "input")) { if (!json_raw_value_replace(p, &input)) { free(key); goto bad; } } else if (!strcmp(key, "content")) { if (!json_raw_value_replace(p, &tool_result)) { free(key); goto bad; } } else if (!strcmp(key, "source")) { if (!parse_anthropic_image_source(p, &source_type, &media_type, &image_data)) { free(key); goto bad; } } else if (!json_skip_value(p)) { free(key); goto bad; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto bad; (*p)++; /* JSON object member order is not meaningful. Some Anthropic-compatible * clients serialize a message as {"content": ..., "role": ...}, so the * caller may not know the enclosing role yet while parsing content blocks. * Classify protocol blocks by their own "type" field; later rendering and * validation use the final message role. */ if (!allow_tools && (!type || (strcmp(type, "text") && strcmp(type, "image")))) goto bad; if (type && !strcmp(type, "tool_use")) { tool_call tc = {0}; tc.id = id ? xstrdup(id) : NULL; tc.name = name ? xstrdup(name) : xstrdup(""); tc.arguments = input ? xstrdup(input) : xstrdup("{}"); tool_calls_push(&msg->calls, tc); } else if (type && !strcmp(type, "tool_result")) { chat_msg nested = {0}; const char *content = tool_result ? tool_result : "\"\""; if (!parse_anthropic_content(&content, &nested, false)) { chat_msg_free(&nested); goto bad; } chat_msg_add_tool_call_id(msg, id); buf b = {0}; buf_puts(&b, msg->content ? msg->content : ""); const size_t begin = b.len; buf_puts(&b, ""); append_tool_result_text(&b, nested.content); buf_puts(&b, ""); /* Preserve parsed boundaries, not just markup which ordinary user * text can also contain. V4.1 orders and separates these blocks. */ msg->tool_results = xrealloc(msg->tool_results, (size_t)(msg->tool_results_len + 1) * sizeof(msg->tool_results[0])); msg->tool_results[msg->tool_results_len++] = (tool_result_span){ .begin = begin, .end = b.len, .id = id ? xstrdup(id) : NULL, .content = nested.content}; nested.content = NULL; free(msg->content); msg->content = buf_take(&b); if (nested.images.len) { size_t total = msg->images.len + nested.images.len; msg->images.v = xrealloc(msg->images.v, total * sizeof(msg->images.v[0])); memcpy(msg->images.v + msg->images.len, nested.images.v, nested.images.len * sizeof(msg->images.v[0])); msg->images.len = msg->images.cap = total; nested.images.len = 0; } chat_msg_free(&nested); } else if (type && !strcmp(type, "image")) { char marker[SERVER_IMAGE_MARKER_BYTES]; if (!source_type || strcmp(source_type, "base64") || !server_image_inputs_push_base64(&msg->images, media_type, image_data, marker)) goto bad; append_owned_text(&msg->content, marker); } else { if (text) { buf b = {0}; buf_puts(&b, msg->content ? msg->content : ""); append_anthropic_block_content(&b, text); free(msg->content); msg->content = buf_take(&b); } if (thinking) { buf b = {0}; buf_puts(&b, msg->reasoning ? msg->reasoning : ""); append_anthropic_block_content(&b, thinking); free(msg->reasoning); msg->reasoning = buf_take(&b); } } free(type); free(text); free(thinking); free(id); free(name); free(input); free(tool_result); free(source_type); free(media_type); free(image_data); return true; bad: free(type); free(text); free(thinking); free(id); free(name); free(input); free(tool_result); free(source_type); free(media_type); free(image_data); return false; } static bool parse_anthropic_content(const char **p, chat_msg *msg, bool allow_tools) { json_ws(p); if (**p == '"') return json_string(p, &msg->content); if (json_lit(p, "null")) { msg->content = xstrdup(""); return true; } if (**p != '[') return allow_tools ? json_skip_value(p) : false; (*p)++; json_ws(p); while (**p && **p != ']') { if (**p == '"') { char *s = NULL; if (!json_string(p, &s)) return false; buf b = {0}; buf_puts(&b, msg->content ? msg->content : ""); buf_puts(&b, s); free(msg->content); msg->content = buf_take(&b); free(s); } else if (**p == '{') { if (!parse_anthropic_content_block(p, allow_tools, msg)) return false; } else if (!json_skip_value(p)) { return false; } json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') return false; (*p)++; if (!msg->content) msg->content = xstrdup(""); return true; } static bool parse_anthropic_messages(const char **p, chat_msgs *msgs) { json_ws(p); if (**p != '[') return false; (*p)++; json_ws(p); while (**p && **p != ']') { if (**p != '{') return false; (*p)++; chat_msg msg = {0}; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto fail; json_ws(p); if (**p != ':') { free(key); goto fail; } (*p)++; if (!strcmp(key, "role")) { if (!json_string_replace(p, &msg.role)) { free(key); goto fail; } } else if (!strcmp(key, "content")) { /* Duplicate content members would leave stale block offsets * and tool IDs. Refuse an ambiguous protocol message. */ if (msg.content || msg.calls.len || msg.tool_results_len) { free(key); goto fail; } if (!parse_anthropic_content(p, &msg, true)) { free(key); goto fail; } } else if (!json_skip_value(p)) { free(key); goto fail; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') goto fail; (*p)++; if (!msg.role) msg.role = xstrdup("user"); if (!msg.content) msg.content = xstrdup(""); if (msg.images.len && strcmp(msg.role, "user")) goto fail; chat_msgs_push(msgs, msg); memset(&msg, 0, sizeof(msg)); json_ws(p); if (**p == ',') (*p)++; json_ws(p); continue; fail: chat_msg_free(&msg); return false; } if (**p != ']') return false; (*p)++; return true; } static bool anthropic_system_part_is_private(const char *s) { return s && !strncmp(s, "x-anthropic-", 12); } static void append_anthropic_system_part(buf *b, const char *s) { if (!s || !s[0] || anthropic_system_part_is_private(s)) return; if (b->len && b->ptr[b->len - 1] != '\n') buf_putc(b, '\n'); buf_puts(b, s); } static bool parse_anthropic_system_object(const char **p, buf *out) { if (**p != '{') return false; (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; if (!strcmp(key, "text")) { char *text = NULL; if (!json_string(p, &text)) { free(key); return false; } append_anthropic_system_part(out, text); free(text); } else if (!json_skip_value(p)) { free(key); return false; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } static bool parse_anthropic_system(const char **p, char **out) { json_ws(p); buf b = {0}; if (**p == '"') { char *text = NULL; if (!json_string(p, &text)) return false; append_anthropic_system_part(&b, text); free(text); *out = buf_take(&b); return true; } if (json_lit(p, "null")) { *out = xstrdup(""); return true; } if (**p != '[') { if (!json_skip_value(p)) return false; *out = xstrdup(""); return true; } (*p)++; json_ws(p); while (**p && **p != ']') { if (**p == '"') { char *text = NULL; if (!json_string(p, &text)) goto bad; append_anthropic_system_part(&b, text); free(text); } else if (**p == '{') { if (!parse_anthropic_system_object(p, &b)) goto bad; } else if (!json_skip_value(p)) { goto bad; } json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') goto bad; (*p)++; *out = buf_take(&b); return true; bad: buf_free(&b); return false; } static void append_tools_prompt_text(buf *b, const char *tool_schemas, bool v41) { if (!tool_schemas || !tool_schemas[0]) return; if (v41) buf_puts(b, "## Tools\n\n" "You can invoke tools using this format:\n\n" DS41_TOOL_CALLS_START "\n" DS41_INVOKE_START " name=\"$TOOL_NAME\">\n" DS41_PARAM_START " name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE" DS41_PARAM_END "\n" DS41_INVOKE_END "\n" DS41_TOOL_CALLS_END "\n\n" "String values use string=\"true\"; all other values use JSON and string=\"false\". " "Inside string values only, escape a literal as </|DSML| parameter>. " "To write that escaped spelling literally, use &lt;/|DSML| parameter>. Other HTML entities are unchanged.\n\n" "Finish reasoning with before tool calls or a final response.\n\n" "### Available Tool Schemas\n\n"); else buf_puts(b, "## Tools\n\n" "You have access to a set of tools to help answer the user question. " "You can invoke tools by writing a \"<|DSML|tool_calls>\" block like the following:\n\n" "<|DSML|tool_calls>\n" "<|DSML|invoke name=\"$TOOL_NAME\">\n" "<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE\n" "...\n" "\n" "<|DSML|invoke name=\"$TOOL_NAME2\">\n" "...\n" "\n" "\n\n" "String parameters should be specified as raw text and set `string=\"true\"`. " "Preserve characters such as `>`, `&`, and `&&` exactly; never replace normal string characters with XML or HTML entity escapes. " "Only if a string value itself contains the exact closing parameter tag ``, write that tag as `</|DSML|parameter>` inside the value. " "For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string=\"false\"`.\n\n" "When thinking mode is enabled, finish reasoning with before any tool calls or final response.\n\n" "Otherwise, output directly after with tool calls or final response.\n\n" "### Available Tool Schemas\n\n"); buf_puts(b, tool_schemas); buf_puts(b, "\n\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls. " "Use the exact parameter names from the schemas."); } static void json_escape(buf *b, const char *s); typedef struct { char *key; char *value; bool is_string; bool used; } json_arg; typedef struct { json_arg *v; int len; int cap; } json_args; static void json_args_free(json_args *args) { for (int i = 0; i < args->len; i++) { free(args->v[i].key); free(args->v[i].value); } free(args->v); memset(args, 0, sizeof(*args)); } static void json_args_push(json_args *args, json_arg arg) { if (args->len == args->cap) { args->cap = args->cap ? args->cap * 2 : 8; args->v = xrealloc(args->v, (size_t)args->cap * sizeof(args->v[0])); } args->v[args->len++] = arg; } static int json_args_find_unused(json_args *args, const char *key) { if (!key) return -1; for (int i = 0; i < args->len; i++) { if (!args->v[i].used && args->v[i].key && !strcmp(args->v[i].key, key)) return i; } return -1; } static bool json_args_parse(const char *json, json_args *args) { const char *p = json ? json : ""; json_ws(&p); if (*p != '{') return false; p++; json_ws(&p); while (*p && *p != '}') { bool is_string = false; char *key = NULL; char *value = NULL; if (!json_string(&p, &key)) goto bad; json_ws(&p); if (*p != ':') goto bad; p++; json_ws(&p); if (*p == '"') { is_string = true; if (!json_string(&p, &value)) goto bad; } else { char *raw = NULL; if (!json_raw_value(&p, &raw)) goto bad; value = json_minify_raw_value(raw); free(raw); } json_arg arg = {.key = key, .value = value, .is_string = is_string}; json_args_push(args, arg); key = value = NULL; json_ws(&p); if (*p == ',') p++; json_ws(&p); continue; bad: free(key); free(value); json_args_free(args); return false; } if (*p != '}') { json_args_free(args); return false; } return true; } static bool append_glm_tool_schema_json(buf *b, const char *json, bool *emitted) { json_args args = {0}; *emitted = false; if (!json_args_parse(json, &args)) return false; for (int i = 0; i < args.len; i++) { const json_arg *arg = &args.v[i]; if (!strcmp(arg->key, "defer_loading") && !arg->is_string && !strcmp(arg->value, "true")) { json_args_free(&args); return true; } } buf_putc(b, '{'); bool wrote = false; for (int i = 0; i < args.len; i++) { const json_arg *arg = &args.v[i]; if (!strcmp(arg->key, "defer_loading") || !strcmp(arg->key, "strict")) continue; if (wrote) buf_puts(b, ", "); json_escape(b, arg->key); buf_puts(b, ": "); if (arg->is_string) json_escape(b, arg->value); else buf_puts(b, arg->value); wrote = true; } buf_putc(b, '}'); json_args_free(&args); *emitted = true; return true; } static void append_glm_tools_prompt_text(buf *b, const char *tool_schemas) { if (!tool_schemas || !tool_schemas[0]) return; buf_puts(b, "\n# Tools\n\n" "You may call one or more functions to assist with the user query.\n\n" "You are provided with function signatures within XML tags:\n" "\n\n"); const char *p = tool_schemas; bool any = false; json_ws(&p); while (*p) { char *raw = NULL; if (!json_raw_value(&p, &raw)) break; bool emitted = false; if (!append_glm_tool_schema_json(b, raw, &emitted)) { buf_puts(b, raw); emitted = true; } free(raw); if (emitted) { buf_puts(b, "\n\n\n"); any = true; } json_ws(&p); } if (!any) buf_putc(b, '\n'); buf_puts(b, "\n\n" "For each function call, output the function name and arguments within the following XML format:\n" "{function-name}" "{arg-key-1}" "{arg-value-1}" "{arg-key-2}" "{arg-value-2}" "..."); } static void append_dsml_attr_escaped(buf *b, const char *s) { for (s = s ? s : ""; *s; s++) { if (*s == '&') buf_puts(b, "&"); else if (*s == '<') buf_puts(b, "<"); else if (*s == '>') buf_puts(b, ">"); else if (*s == '"') buf_puts(b, """); else buf_putc(b, *s); } } static void append_dsml_parameter_text(buf *b, const char *s, bool v41) { const char *end = v41 ? DS41_PARAM_END : ""; for (s = s ? s : ""; *s;) { if (ds4_tool_text_needs_escape(s, end)) { buf_puts(b, *s == '<' ? "<" : "&"); s++; } else { buf_putc(b, *s++); } } } static void append_glm_tag_body_text(buf *b, const char *s, const char *end) { for (s = s ? s : ""; *s;) { if (ds4_tool_text_needs_escape(s, end)) { buf_puts(b, *s == '<' ? "<" : "&"); s++; } else { buf_putc(b, *s++); } } } static void append_glm_arg_key_text(buf *b, const char *s) { append_glm_tag_body_text(b, s, ""); } static void append_glm_arg_value_text(buf *b, const char *s) { append_glm_tag_body_text(b, s, ""); } static void append_glm_tool_response_text(buf *b, const char *s) { append_glm_tag_body_text(b, s, ""); } static bool chat_msg_is_glm_tool_result(const chat_msg *m) { if (!m || !m->role) return false; if (!strcmp(m->role, "tool") || !strcmp(m->role, "function")) return true; /* Anthropic carries tool_result blocks in a user-role message. Its parser * records the call ids and preserves each block with a * wrapper, which lets the GLM renderer distinguish it from user text. */ return !strcmp(m->role, "user") && m->tool_call_ids_len > 0; } static void append_glm_tool_response(buf *b, const char *text, size_t len) { char *body = xstrndup(text ? text : "", len); buf_puts(b, ""); append_glm_tool_response_text(b, body); buf_puts(b, ""); free(body); } static void append_glm_tool_result_message(buf *b, const chat_msg *m) { const char *content = m && m->content ? m->content : ""; if (m && m->role && !strcmp(m->role, "user") && m->tool_call_ids_len > 0) { static const char open[] = ""; static const char close[] = ""; const char *p = content; bool found = false; while ((p = strstr(p, open)) != NULL) { const char *body = p + sizeof(open) - 1; const char *end = strstr(body, close); if (!end) break; append_glm_tool_response(b, body, (size_t)(end - body)); found = true; p = end + sizeof(close) - 1; } if (found) return; } append_glm_tool_response(b, content, strlen(content)); } static void append_tool_result_text(buf *b, const char *s) { /* Tool output is data. DeepSeek's renderer keeps it as ordinary text inside * ..., so preserving literal '<', '>' and '&' is * important for read-file tools and shell output. The only delimiter we must * protect is the wrapper's own closing tag; otherwise a file containing that * exact sentinel would terminate the result early. */ const char *end = ""; for (s = s ? s : ""; *s;) { if (ds4_tool_text_needs_escape(s, end)) { buf_puts(b, *s == '<' ? "<" : "&"); s++; } else { buf_putc(b, *s++); } } } static void append_dsml_json_literal(buf *b, const char *s, bool v41) { const char *end = v41 ? DS41_PARAM_END : ""; const size_t endlen = strlen(end); for (s = s ? s : ""; *s;) { if (!strncmp(s, end, endlen)) { buf_puts(b, "\\u003c"); s++; } else { buf_putc(b, *s++); } } } static void append_dsml_arg(buf *b, const json_arg *arg, bool v41) { buf_puts(b, v41 ? DS41_PARAM_START " name=\"" : "<|DSML|parameter name=\""); append_dsml_attr_escaped(b, arg->key); buf_puts(b, "\" string=\""); buf_puts(b, arg->is_string ? "true" : "false"); buf_puts(b, "\">"); if (arg->is_string) append_dsml_parameter_text(b, arg->value, v41); else append_dsml_json_literal(b, arg->value, v41); buf_puts(b, v41 ? DS41_PARAM_END "\n" : "\n"); } static void append_glm_arg(buf *b, const json_arg *arg) { buf_puts(b, ""); append_glm_arg_key_text(b, arg->key); buf_puts(b, ""); append_glm_arg_value_text(b, arg->value); buf_puts(b, ""); } static bool append_dsml_arguments_from_json(buf *b, const char *json, const tool_schema_order *order, bool v41) { json_args args = {0}; if (!json_args_parse(json, &args)) return false; if (order) { for (int i = 0; i < order->len; i++) { int idx = json_args_find_unused(&args, order->prop[i]); if (idx < 0) continue; append_dsml_arg(b, &args.v[idx], v41); args.v[idx].used = true; } } for (int i = 0; i < args.len; i++) { if (args.v[i].used) continue; append_dsml_arg(b, &args.v[i], v41); } json_args_free(&args); return true; } static bool append_glm_arguments_from_json(buf *b, const char *json, const tool_schema_order *order) { json_args args = {0}; if (!json_args_parse(json, &args)) return false; if (order) { for (int i = 0; i < order->len; i++) { int idx = json_args_find_unused(&args, order->prop[i]); if (idx < 0) continue; append_glm_arg(b, &args.v[idx]); args.v[idx].used = true; } } for (int i = 0; i < args.len; i++) { if (args.v[i].used) continue; append_glm_arg(b, &args.v[i]); } json_args_free(&args); return true; } static void append_json_arg_pair(buf *b, const json_arg *arg) { json_escape(b, arg->key); buf_puts(b, ":"); if (arg->is_string) json_escape(b, arg->value); else buf_puts(b, arg->value); } static void append_json_object_or_empty(buf *b, const char *json) { json_args args = {0}; if (!json_args_parse(json, &args)) { buf_puts(b, "{}"); return; } buf_putc(b, '{'); bool wrote = false; for (int i = 0; i < args.len; i++) { if (wrote) buf_putc(b, ','); append_json_arg_pair(b, &args.v[i]); wrote = true; } buf_putc(b, '}'); json_args_free(&args); } static void append_dsml_tool_calls_text(buf *b, const tool_calls *calls, bool v41) { if (!calls || calls->len == 0) return; if (calls->raw_tool_text && calls->raw_tool_text[0]) { buf_puts(b, calls->raw_tool_text); return; } buf_puts(b, v41 ? "\n\n" DS41_TOOL_CALLS_START "\n" : "\n\n<|DSML|tool_calls>\n"); for (int i = 0; i < calls->len; i++) { const tool_call *tc = &calls->v[i]; buf_puts(b, v41 ? DS41_INVOKE_START " name=\"" : "<|DSML|invoke name=\""); append_dsml_attr_escaped(b, tc->name); buf_puts(b, "\">\n"); if (!append_dsml_arguments_from_json(b, tc->arguments, NULL, v41)) { buf_puts(b, v41 ? DS41_PARAM_START " name=\"arguments\" string=\"true\">" : "<|DSML|parameter name=\"arguments\" string=\"true\">"); append_dsml_parameter_text(b, tc->arguments, v41); buf_puts(b, v41 ? DS41_PARAM_END "\n" : "\n"); } buf_puts(b, v41 ? DS41_INVOKE_END "\n" : "\n"); } buf_puts(b, v41 ? DS41_TOOL_CALLS_END : ""); } static void append_glm_tool_calls_text(buf *b, const tool_calls *calls, const tool_schema_orders *tool_orders) { if (!calls || calls->len == 0) return; if (calls->raw_tool_text && calls->raw_tool_text[0]) { buf_puts(b, calls->raw_tool_text); return; } /* GLM's chat template emits the block inline, with no newline before or * after it, so the reconstruction must not add one either: the rendered * turn has to stay byte-identical to the sampled bytes. */ for (int i = 0; i < calls->len; i++) { const tool_call *tc = &calls->v[i]; const tool_schema_order *order = tool_schema_orders_find(tool_orders, tc->name); buf_puts(b, ""); buf_puts(b, tc->name ? tc->name : ""); if (!append_glm_arguments_from_json(b, tc->arguments, order)) { buf_puts(b, "arguments"); append_glm_arg_value_text(b, tc->arguments); buf_puts(b, ""); } buf_puts(b, ""); } } /* Parameter values render as the template does: strings verbatim, other * JSON values as their (minified) JSON text. */ static void append_qwen_tool_calls_text(buf *b, const tool_calls *calls, bool has_content, const tool_schema_orders *tool_orders) { if (!calls || calls->len == 0) return; if (calls->raw_tool_text && calls->raw_tool_text[0]) { buf_puts(b, calls->raw_tool_text); return; } for (int i = 0; i < calls->len; i++) { const tool_call *tc = &calls->v[i]; const tool_schema_order *order = tool_schema_orders_find(tool_orders, tc->name); if (i == 0) buf_puts(b, has_content ? "\n\n\n\n\nname ? tc->name : ""); buf_puts(b, ">\n"); json_args args = {0}; if (json_args_parse(tc->arguments, &args)) { for (int pass = 0; pass < 2; pass++) { const int n = pass == 0 ? (order ? order->len : 0) : args.len; for (int k = 0; k < n; k++) { int idx = k; if (pass == 0) { idx = json_args_find_unused(&args, order->prop[k]); if (idx < 0) continue; } else if (args.v[k].used) { continue; } const json_arg *arg = &args.v[idx]; args.v[idx].used = true; buf_puts(b, "key, ">"); buf_puts(b, ">\n"); if (arg->is_string) { append_glm_tag_body_text(b, arg->value, ""); } else { char *min = json_minify_raw_value(arg->value ? arg->value : "null"); append_glm_tag_body_text(b, min && min[0] ? min : "null", ""); free(min); } buf_puts(b, "\n\n"); } } json_args_free(&args); } else if (tc->arguments && tc->arguments[0]) { buf_puts(b, "\n"); append_glm_tag_body_text(b, tc->arguments, ""); buf_puts(b, "\n\n"); } buf_puts(b, "\n"); } } static void append_tool_calls_text_for_syntax(buf *b, server_model_syntax syntax, const tool_calls *calls, const tool_schema_orders *tool_orders) { if (syntax == SERVER_MODEL_SYNTAX_GLM) { append_glm_tool_calls_text(b, calls, tool_orders); } else if (syntax == SERVER_MODEL_SYNTAX_QWEN) { append_qwen_tool_calls_text(b, calls, b->len > 0, tool_orders); } else { append_dsml_tool_calls_text(b, calls, syntax == SERVER_MODEL_SYNTAX_DEEPSEEK41); } } static bool role_is_system(const char *role) { return !strcmp(role, "system") || !strcmp(role, "developer"); } static bool role_is_user_like(const char *role) { return !strcmp(role, "user") || !strcmp(role, "tool") || !strcmp(role, "function"); } static bool chat_history_uses_tool_context(const chat_msgs *msgs, const char *tool_schemas) { if (tool_schemas && tool_schemas[0]) return true; for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if ((!strcmp(m->role, "assistant") && m->calls.len > 0) || !strcmp(m->role, "tool") || !strcmp(m->role, "function")) { return true; } } return false; } static char *render_deepseek_chat_prompt_text(const chat_msgs *msgs, const char *tool_schemas, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { (void)tool_orders; const bool think = ds4_think_mode_enabled(think_mode); const bool tool_context = chat_history_uses_tool_context(msgs, tool_schemas); int last_user_idx = -1; buf system = {0}; /* Render tool schemas before the client system content so * --kv-cache-boundary-trim-tokens chops a dynamic tail from the client * message instead of the much larger tool-schema region. */ if (tool_schemas && tool_schemas[0]) { append_tools_prompt_text(&system, tool_schemas, false); } for (int i = 0; i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (!role_is_system(m->role)) continue; if (system.len) buf_puts(&system, "\n\n"); buf_puts(&system, m->content ? m->content : ""); } for (int i = 0; i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_user_like(m->role)) last_user_idx = i; } buf out = {0}; buf_puts(&out, "<|begin▁of▁sentence|>"); if (think_mode == DS4_THINK_MAX) buf_puts(&out, ds4_think_max_prefix()); buf_puts(&out, system.ptr ? system.ptr : ""); bool pending_assistant = false; bool pending_tool_result = false; for (int i = 0; i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_system(m->role)) { continue; } else if (!strcmp(m->role, "user")) { buf_puts(&out, "<|User|>"); buf_puts(&out, m->content ? m->content : ""); pending_assistant = true; pending_tool_result = false; } else if (!strcmp(m->role, "tool") || !strcmp(m->role, "function")) { if (!pending_tool_result) buf_puts(&out, "<|User|>"); buf_puts(&out, ""); append_tool_result_text(&out, m->content); buf_puts(&out, ""); pending_assistant = true; pending_tool_result = true; } else if (!strcmp(m->role, "assistant")) { if (pending_assistant) { buf_puts(&out, "<|Assistant|>"); if (think) { if (tool_context || i > last_user_idx) { buf_puts(&out, ""); buf_puts(&out, m->reasoning ? m->reasoning : ""); buf_puts(&out, ""); } else { buf_puts(&out, ""); } } else { buf_puts(&out, ""); } } buf_puts(&out, m->content ? m->content : ""); append_dsml_tool_calls_text(&out, &m->calls, false); buf_puts(&out, "<|end▁of▁sentence|>"); pending_assistant = false; pending_tool_result = false; } } if (pending_assistant) { buf_puts(&out, "<|Assistant|>"); buf_puts(&out, think ? "" : ""); } buf_free(&system); return buf_take(&out); } static bool text_starts_with_think_tag(const char *s) { return s && (!strncmp(s, "", 7) || !strncmp(s, "", 8)); } static void append_trimmed_text(buf *out, const char *text) { const unsigned char *start = (const unsigned char *)(text ? text : ""); while (*start && isspace(*start)) start++; const unsigned char *end = start + strlen((const char *)start); while (end > start && isspace(end[-1])) end--; buf_append(out, (const char *)start, (size_t)(end - start)); } static void append_glm_assistant_message_prefix(buf *out, const chat_msg *m, bool preserve_reasoning) { const char *content = m && m->content ? m->content : ""; if (text_starts_with_think_tag(content)) return; if (preserve_reasoning) { buf_puts(out, ""); buf_puts(out, m && m->reasoning ? m->reasoning : ""); buf_puts(out, ""); } else { buf_puts(out, ""); } } static char *render_glm_chat_prompt_text(const chat_msgs *msgs, const char *tool_schemas, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { const bool think = ds4_think_mode_enabled(think_mode); const bool tool_context = chat_history_uses_tool_context(msgs, tool_schemas); int last_user_idx = -1; for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_user_like(m->role)) last_user_idx = i; } buf out = {0}; buf_puts(&out, "[gMASK]"); if (think) { const char *effort = ds4_glm_reasoning_effort_text(think_mode); buf_puts(&out, "<|system|>"); buf_puts(&out, effort ? effort : "Reasoning Effort: Max"); } if (tool_schemas && tool_schemas[0]) { buf tools = {0}; append_glm_tools_prompt_text(&tools, tool_schemas); if (tools.len) { buf_puts(&out, "<|system|>"); buf_puts(&out, tools.ptr); } buf_free(&tools); } for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (!role_is_system(m->role)) continue; buf_puts(&out, "<|system|>"); buf_puts(&out, m->content ? m->content : ""); } bool pending_assistant = false; bool observation_open = false; for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_system(m->role)) { observation_open = false; continue; } else if (chat_msg_is_glm_tool_result(m)) { if (!observation_open) buf_puts(&out, "<|observation|>"); append_glm_tool_result_message(&out, m); observation_open = true; pending_assistant = true; } else if (!strcmp(m->role, "user")) { observation_open = false; buf_puts(&out, "<|user|>"); buf_puts(&out, m->content ? m->content : ""); pending_assistant = true; } else if (!strcmp(m->role, "assistant")) { observation_open = false; (void)pending_assistant; buf_puts(&out, "<|assistant|>"); append_glm_assistant_message_prefix( &out, m, think && (tool_context || i > last_user_idx)); append_trimmed_text(&out, m->content); append_tool_calls_text_for_syntax(&out, SERVER_MODEL_SYNTAX_GLM, &m->calls, tool_orders); pending_assistant = false; } } if (pending_assistant) { buf_puts(&out, "<|assistant|>"); buf_puts(&out, think ? "" : ""); } return buf_take(&out); } typedef struct { const chat_msg *msg; size_t order, position; } ds41_tool_result_order; /* Most views borrow the original message. Only split Anthropic blocks own * strings, kept in fragments so the source history and image list stay intact. */ static chat_msgs ds41_message_views(const chat_msgs *msgs, chat_msgs *fragments, int *start) { chat_msgs views = {0}; const int first = *start; for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (i == first) *start = views.len; if (strcmp(m->role, "user") || !m->tool_results_len) { chat_msgs_push(&views, *m); continue; } size_t begin = 0, length = strlen(m->content); for (int j = 0; j <= m->tool_results_len; j++) { const tool_result_span *span = j < m->tool_results_len ? &m->tool_results[j] : NULL; const size_t end = span ? span->begin : length; if (end > begin) { chat_msg text = {.role = xstrdup("user"), .content = xstrndup(m->content + begin, end - begin)}; chat_msgs_push(fragments, text); chat_msgs_push(&views, text); } if (span) { chat_msg result = {.role = xstrdup("tool"), .content = xstrdup(span->content ? span->content : ""), .tool_call_id = span->id ? xstrdup(span->id) : NULL}; chat_msgs_push(fragments, result); chat_msgs_push(&views, result); begin = span->end; } } } if (!msgs || first >= msgs->len) *start = views.len; return views; } static int ds41_compare_tool_results(const void *a, const void *b) { const ds41_tool_result_order *x = a, *y = b; if (x->order != y->order) return x->order < y->order ? -1 : 1; return (x->position > y->position) - (x->position < y->position); } /* Parallel tool replies can arrive out of order. V4.1 renders them in * call order within each merged user turn, leaving ordinary text in place. */ static const chat_msg **ds41_order_messages(const chat_msgs *msgs, const stop_list *initial_order) { if (!msgs || !msgs->len) return NULL; const chat_msg **ordered = xmalloc((size_t)msgs->len * sizeof(*ordered)); ds41_tool_result_order *results = xmalloc((size_t)msgs->len * sizeof(*results)); rax *order = raxNew(); for (int i = 0; initial_order && i < initial_order->len; i++) { const char *id = initial_order->v[i]; raxInsert(order, (unsigned char *)id, strlen(id), (void *)(uintptr_t)(i + 1), NULL); } for (int i = 0; i < msgs->len;) { const chat_msg *m = &msgs->v[i]; ordered[i] = m; if (!strcmp(m->role, "assistant") && m->calls.len) { raxFree(order); order = raxNew(); for (int j = 0; j < m->calls.len; j++) { const char *id = m->calls.v[j].id; if (id && id[0]) raxInsert(order, (unsigned char *)id, strlen(id), (void *)(uintptr_t)(j + 1), NULL); } } if (!role_is_user_like(m->role)) { i++; continue; } const int begin = i; size_t count = 0; while (i < msgs->len && role_is_user_like(msgs->v[i].role)) { m = ordered[i] = &msgs->v[i]; if (strcmp(m->role, "user")) { const char *id = m->tool_call_id; void *rank = id ? raxFind(order, (unsigned char *)id, strlen(id)) : raxNotFound; results[count++] = (ds41_tool_result_order){.msg = m, .order = rank == raxNotFound ? 0 : (uintptr_t)rank - 1, .position = (size_t)i}; } i++; } qsort(results, count, sizeof(*results), ds41_compare_tool_results); size_t next = 0; for (int j = begin; j < i; j++) if (strcmp(msgs->v[j].role, "user")) ordered[j] = results[next++].msg; } raxFree(order); free(results); return ordered; } /* The same renderer owns full requests and live tool-result tails, so their * separators and role transitions cannot disagree at the cache boundary. */ static char *render_deepseek41_chat_ordered(const chat_msgs *msgs, int start, const char *tool_schemas, ds4_think_mode mode, bool live_tail, const stop_list *initial_order) { buf out = {0}; chat_msgs fragments = {0}; chat_msgs views = ds41_message_views(msgs, &fragments, &start); msgs = &views; const chat_msg **ordered = ds41_order_messages(msgs, initial_order); const bool think = ds4_think_mode_enabled(mode); const bool keep_reasoning = live_tail || chat_history_uses_tool_context(msgs, tool_schemas); const char *effort = ds4_deepseek41_reasoning_effort_text(mode); bool initial_system = false, pending = false, user_open = false; int last_user = -1; for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_user_like(m->role) || (i > 0 && role_is_system(m->role))) last_user = i; } if (live_tail) { buf_puts(&out, "<|end▁of▁sentence|>"); } else { buf_puts(&out, "<|begin▁of▁sentence|>"); if (effort || (tool_schemas && tool_schemas[0])) { buf_puts(&out, "<|System|>"); initial_system = true; if (effort) buf_puts(&out, effort); if (tool_schemas && tool_schemas[0]) append_tools_prompt_text(&out, tool_schemas, true); } } for (int i = start; msgs && i < msgs->len; i++) { const chat_msg *m = ordered[i]; if (role_is_system(m->role)) { if (i == 0 && initial_system) { if (tool_schemas && tool_schemas[0]) buf_puts(&out, "\n\n"); } else buf_puts(&out, "<|System|>"); buf_puts(&out, m->content ? m->content : ""); pending = i > 0 || live_tail; user_open = false; } else if (role_is_user_like(m->role)) { buf_puts(&out, user_open ? "\n\n" : "<|User|>"); if (strcmp(m->role, "user")) { buf_puts(&out, ""); append_tool_result_text(&out, m->content); buf_puts(&out, ""); } else buf_puts(&out, m->content ? m->content : ""); pending = user_open = true; } else if (!strcmp(m->role, "assistant")) { if (pending) { buf_puts(&out, "<|Assistant|>"); if (think && (keep_reasoning || i > last_user)) { buf_puts(&out, ""); buf_puts(&out, m->reasoning ? m->reasoning : ""); } buf_puts(&out, ""); } buf_puts(&out, m->content ? m->content : ""); append_dsml_tool_calls_text(&out, &m->calls, true); buf_puts(&out, "<|end▁of▁sentence|>"); pending = user_open = false; } } if (pending) { buf_puts(&out, "<|Assistant|>"); buf_puts(&out, think ? "" : ""); } free(ordered); free(views.v); chat_msgs_free(&fragments); return buf_take(&out); } static char *render_deepseek41_chat(const chat_msgs *msgs, int start, const char *tool_schemas, ds4_think_mode mode, bool live_tail) { return render_deepseek41_chat_ordered(msgs, start, tool_schemas, mode, live_tail, NULL); } static void append_qwen_tools_prompt_text(buf *b, const char *tool_schemas) { buf_puts(b, "# Tools\n\nYou have access to the following functions:\n\n"); const char *p = tool_schemas ? tool_schemas : ""; json_ws(&p); while (*p) { char *raw = NULL; if (!json_raw_value(&p, &raw)) break; buf inner = {0}; bool emitted = false; if (!append_glm_tool_schema_json(&inner, raw, &emitted)) { buf_puts(&inner, raw); emitted = true; } if (emitted) { buf_puts(b, "\n{\"type\": \"function\", \"function\": "); buf_puts(b, inner.ptr ? inner.ptr : "{}"); buf_putc(b, '}'); } buf_free(&inner); free(raw); json_ws(&p); } buf_puts(b, "\n\n\n" "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" "\n\n\nvalue_1\n\n" "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" "\n\n\n\n\nReminder:\n" "- Function calls MUST follow the specified format: an inner block must be nested " "within XML tags\n" "- Required parameters MUST be specified\n" "- You may provide optional reasoning for your function call in natural language BEFORE the function call, " "but NOT after\n" "- If there is no function call available, answer the question like normal with your current knowledge and " "do not tell the user about function calls\n"); } static void append_qwen_tool_response(buf *b, const char *text, size_t len) { char *body = xstrndup(text ? text : "", len); buf_puts(b, "\n\n"); append_glm_tag_body_text(b, body, ""); buf_puts(b, "\n"); free(body); } static void append_qwen_tool_result_message(buf *b, const chat_msg *m) { const char *content = m && m->content ? m->content : ""; if (m && m->role && !strcmp(m->role, "user") && m->tool_call_ids_len > 0) { static const char open[] = ""; static const char close[] = ""; const char *p = content; bool found = false; while ((p = strstr(p, open)) != NULL) { const char *body = p + sizeof(open) - 1; const char *end = strstr(body, close); if (!end) break; append_qwen_tool_response(b, body, (size_t)(end - body)); found = true; p = end + sizeof(close) - 1; } if (found) return; } append_qwen_tool_response(b, content, strlen(content)); } static void append_qwen_assistant_message(buf *out, const chat_msg *m, const tool_schema_orders *tool_orders) { const char *content = m && m->content ? m->content : ""; const char *reasoning = m && m->reasoning ? m->reasoning : ""; const bool has_calls = m && m->calls.len > 0; buf body = {0}; append_trimmed_text(&body, content); const bool has_content = body.len > 0; if (has_content && !has_calls) { /* keep a plain answer's trailing whitespace so the echoed turn * re-tokenizes to the sampled tokens */ const char *end = content + strlen(content); while (end > content && isspace((unsigned char)end[-1])) end--; buf_append(&body, end, strlen(end)); } buf_puts(out, "<|im_start|>assistant\n"); if (!text_starts_with_think_tag(content)) { /* an empty reasoning block is exactly the no-thinking generation prompt */ buf_puts(out, "\n"); append_trimmed_text(out, reasoning); /* sampled tool text already carries what the model emitted after its * own ; the template separator would double it */ const bool sampled_after_think = !has_content && reasoning[0] && m->calls.raw_tool_text && m->calls.raw_tool_text[0]; buf_puts(out, sampled_after_think ? "\n" : "\n\n\n"); } buf_puts(out, body.ptr ? body.ptr : ""); buf_free(&body); append_qwen_tool_calls_text(out, m ? &m->calls : NULL, has_content, tool_orders); buf_puts(out, "<|im_end|>\n"); } static void append_qwen_generation_prompt(buf *out, bool think) { buf_puts(out, think ? "<|im_start|>assistant\n\n" : "<|im_start|>assistant\n\n\n\n\n"); } static void append_qwen_conversation(buf *out, const chat_msgs *msgs, int start, const tool_schema_orders *tool_orders, bool think) { bool pending_assistant = false; bool tool_open = false; for (int i = start; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_system(m->role)) continue; if (chat_msg_is_glm_tool_result(m)) { if (!tool_open) buf_puts(out, "<|im_start|>user"); append_qwen_tool_result_message(out, m); tool_open = true; pending_assistant = true; continue; } if (tool_open) { buf_puts(out, "<|im_end|>\n"); tool_open = false; } if (!strcmp(m->role, "user")) { buf_puts(out, "<|im_start|>user\n"); append_trimmed_text(out, m->content); buf_puts(out, "<|im_end|>\n"); pending_assistant = true; } else if (!strcmp(m->role, "assistant")) { append_qwen_assistant_message(out, m, tool_orders); pending_assistant = false; } } if (tool_open) buf_puts(out, "<|im_end|>\n"); if (pending_assistant) append_qwen_generation_prompt(out, think); } static char *render_qwen_chat_prompt_text(const chat_msgs *msgs, const char *tool_schemas, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { const bool think = ds4_think_mode_enabled(think_mode); const char *effort = think ? ds4_qwen4_reasoning_effort_text(think_mode) : NULL; const bool have_tools = tool_schemas && tool_schemas[0]; buf system = {0}; for (int i = 0; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (!role_is_system(m->role)) continue; if (system.len) buf_puts(&system, "\n\n"); append_trimmed_text(&system, m->content); } buf out = {0}; if (have_tools || system.len || effort) { buf_puts(&out, "<|im_start|>system\n"); bool wrote = false; if (effort) { buf_puts(&out, effort); wrote = true; } if (have_tools) { if (wrote) buf_puts(&out, "\n\n"); append_qwen_tools_prompt_text(&out, tool_schemas); wrote = true; } if (system.len) { if (wrote) buf_puts(&out, "\n\n"); buf_puts(&out, system.ptr); } buf_puts(&out, "<|im_end|>\n"); } buf_free(&system); append_qwen_conversation(&out, msgs, 0, tool_orders, think); return buf_take(&out); } static char *render_chat_prompt_text_for_syntax(server_model_syntax syntax, const chat_msgs *msgs, const char *tool_schemas, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { if (syntax == SERVER_MODEL_SYNTAX_GLM) { return render_glm_chat_prompt_text(msgs, tool_schemas, tool_orders, think_mode); } if (syntax == SERVER_MODEL_SYNTAX_DEEPSEEK41) return render_deepseek41_chat(msgs, 0, tool_schemas, think_mode, false); if (syntax == SERVER_MODEL_SYNTAX_QWEN) { return render_qwen_chat_prompt_text(msgs, tool_schemas, tool_orders, think_mode); } return render_deepseek_chat_prompt_text(msgs, tool_schemas, tool_orders, think_mode); } static DS4_SERVER_MAYBE_UNUSED char *render_chat_prompt_text( const chat_msgs *msgs, const char *tool_schemas, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { return render_chat_prompt_text_for_syntax(SERVER_MODEL_SYNTAX_DEEPSEEK, msgs, tool_schemas, tool_orders, think_mode); } static bool request_tokenize_multimodal_prompt(ds4_engine *e, server *s, request *r, const chat_msgs *msgs, char *err, size_t errlen) { size_t count = 0; for (int i = 0; msgs && i < msgs->len; i++) count += msgs->v[i].images.len; if (count == 0) { ds4_tokenize_rendered_chat(e, r->prompt_text, &r->prompt); return true; } if (count > 16) { snprintf(err, errlen, "too many images; at most 16 are allowed"); return false; } if (!e || !s || !ds4_engine_has_vision(e)) { snprintf(err, errlen, "image input requires starting ds4-server with --vision FILE"); return false; } server_image_input **inputs = xmalloc(count * sizeof(inputs[0])); ds4_vision_embedding *embeddings = xmalloc(count * sizeof(embeddings[0])); memset(embeddings, 0, count * sizeof(embeddings[0])); size_t next = 0; for (int i = 0; i < msgs->len; i++) { for (size_t j = 0; j < msgs->v[i].images.len; j++) inputs[next++] = &msgs->v[i].images.v[j]; } bool ok = true; server_inference_lock(s); for (size_t i = 0; i < count; i++) { if (!server_encode_image(s, inputs[i], &embeddings[i], err, errlen)) { ok = false; break; } } server_inference_unlock(s); if (!ok) goto done; r->images = xmalloc(count * sizeof(r->images[0])); r->image_markers = xmalloc(count * sizeof(r->image_markers[0])); memset(r->images, 0, count * sizeof(r->images[0])); const char *cursor = r->prompt_text; for (size_t i = 0; i < count; i++) { const char *marker = strstr(cursor, inputs[i]->marker); if (!marker) { snprintf(err, errlen, "image marker was lost while rendering the request"); ok = false; break; } char *prefix = xstrndup(cursor, (size_t)(marker - cursor)); ds4_tokenize_rendered_chat(e, prefix, &r->prompt); free(prefix); if (!ds4_prompt_append_vision(e, &r->prompt, &r->images[i], &embeddings[i], err, errlen)) { ok = false; break; } r->image_count++; memcpy(r->image_markers[i], inputs[i]->marker, sizeof(r->image_markers[i])); cursor = marker + strlen(inputs[i]->marker); } if (ok) ds4_tokenize_rendered_chat(e, cursor, &r->prompt); done: for (size_t i = 0; i < count; i++) ds4_vision_embedding_free(&embeddings[i]); free(embeddings); free(inputs); if (!ok) { ds4_tokens_free(&r->prompt); for (size_t i = 0; i < r->image_count; i++) ds4_vision_embedding_free(&r->images[i].embedding); free(r->images); free(r->image_markers); r->image_markers = NULL; r->images = NULL; r->image_count = 0; } return ok; } /* Render only the semantic tail that must be appended to the live KV for a * tool-result continuation. * * In the common agent tool path, the previous assistant tool-call turn is * already in the model session, including hidden thinking and exact sampled * DSML. The next request provides only the tool results, either as OpenAI * Responses tool-output items or Anthropic user content blocks. Re-rendering * the assistant call here would duplicate it and destroy cache alignment, so * this function starts at the first new item and emits only: * * previous EOS, tool results, and the next assistant prefix. * * This is intentionally independent from req.prompt's already-tokenized suffix: * suffix tokenization happens later after the cache decision, using the live * token prefix as the boundary. That avoids BPE merges across the visible * replay/live-KV boundary. */ static char *render_deepseek_live_tool_tail(const chat_msgs *msgs, int start, ds4_think_mode think_mode) { const bool think = ds4_think_mode_enabled(think_mode); buf out = {0}; buf_puts(&out, "<|end▁of▁sentence|>"); bool pending_assistant = false; bool pending_tool_result = false; for (int i = start; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_system(m->role)) { continue; } else if (!strcmp(m->role, "user")) { buf_puts(&out, "<|User|>"); buf_puts(&out, m->content ? m->content : ""); pending_assistant = true; pending_tool_result = false; } else if (!strcmp(m->role, "tool") || !strcmp(m->role, "function")) { if (!pending_tool_result) buf_puts(&out, "<|User|>"); buf_puts(&out, ""); append_tool_result_text(&out, m->content); buf_puts(&out, ""); pending_assistant = true; pending_tool_result = true; } else if (!strcmp(m->role, "assistant")) { if (pending_assistant) { buf_puts(&out, "<|Assistant|>"); if (think) { buf_puts(&out, ""); buf_puts(&out, m->reasoning ? m->reasoning : ""); buf_puts(&out, ""); } else { buf_puts(&out, ""); } } buf_puts(&out, m->content ? m->content : ""); append_dsml_tool_calls_text(&out, &m->calls, false); buf_puts(&out, "<|end▁of▁sentence|>"); pending_assistant = false; pending_tool_result = false; } } if (pending_assistant) { buf_puts(&out, "<|Assistant|>"); buf_puts(&out, think ? "" : ""); } return buf_take(&out); } static char *render_glm_live_tool_tail(const chat_msgs *msgs, int start, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { const bool think = ds4_think_mode_enabled(think_mode); buf out = {0}; bool pending_assistant = false; bool observation_open = false; for (int i = start; msgs && i < msgs->len; i++) { const chat_msg *m = &msgs->v[i]; if (role_is_system(m->role)) { observation_open = false; continue; } else if (chat_msg_is_glm_tool_result(m)) { if (!observation_open) buf_puts(&out, "<|observation|>"); append_glm_tool_result_message(&out, m); observation_open = true; pending_assistant = true; } else if (!strcmp(m->role, "user")) { observation_open = false; buf_puts(&out, "<|user|>"); buf_puts(&out, m->content ? m->content : ""); pending_assistant = true; } else if (!strcmp(m->role, "assistant")) { observation_open = false; buf_puts(&out, "<|assistant|>"); append_glm_assistant_message_prefix(&out, m, think); append_trimmed_text(&out, m->content); append_tool_calls_text_for_syntax(&out, SERVER_MODEL_SYNTAX_GLM, &m->calls, tool_orders); pending_assistant = false; } } if (pending_assistant) { buf_puts(&out, "<|assistant|>"); buf_puts(&out, think ? "" : ""); } return buf_take(&out); } /* Continue a live session after the model's tool-call turn: its <|im_end|> * was the stop token and is not in the KV yet. */ static char *render_qwen_live_tool_tail(const chat_msgs *msgs, int start, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { buf out = {0}; buf_puts(&out, "<|im_end|>\n"); append_qwen_conversation(&out, msgs, start, tool_orders, ds4_think_mode_enabled(think_mode)); return buf_take(&out); } static char *render_live_tool_tail_for_syntax(server_model_syntax syntax, const chat_msgs *msgs, int start, const tool_schema_orders *tool_orders, ds4_think_mode think_mode) { if (syntax == SERVER_MODEL_SYNTAX_GLM) { return render_glm_live_tool_tail(msgs, start, tool_orders, think_mode); } if (syntax == SERVER_MODEL_SYNTAX_DEEPSEEK41) return render_deepseek41_chat(msgs, start, NULL, think_mode, true); if (syntax == SERVER_MODEL_SYNTAX_QWEN) { return render_qwen_live_tool_tail(msgs, start, tool_orders, think_mode); } return render_deepseek_live_tool_tail(msgs, start, think_mode); } static DS4_SERVER_MAYBE_UNUSED char *render_live_tool_tail( const chat_msgs *msgs, int start, ds4_think_mode think_mode) { return render_live_tool_tail_for_syntax(SERVER_MODEL_SYNTAX_DEEPSEEK, msgs, start, NULL, think_mode); } static char *render_request_live_tool_tail(server *s, const request *r, const chat_msgs *msgs, int start, const stop_list *ids) { if (r->model_syntax != SERVER_MODEL_SYNTAX_DEEPSEEK41) return render_live_tool_tail_for_syntax(r->model_syntax, msgs, start, &r->tool_orders, r->think_mode); /* Output-only requests omit the assistant message which declares order. * Copy the remembered order under its mutex, never borrow mutable IDs. */ stop_list order = live_tool_call_order(s, r->api, ids); char *text = render_deepseek41_chat_ordered(msgs, start, NULL, r->think_mode, true, &order); id_list_free(&order); return text; } static void chat_msg_collect_tool_call_ids(const chat_msg *m, stop_list *ids) { if (!m || !ids) return; id_list_push_unique(ids, m->tool_call_id); for (int i = 0; i < m->tool_call_ids_len; i++) { id_list_push_unique(ids, m->tool_call_ids[i]); } } /* Validate Responses tool outputs before rendering. * * A tool output with a call_id is meaningful only if either: * 1. DS4 still has the matching live assistant call in memory, or * 2. the same request replays the prior assistant call item. * * Case 1 is the fast, protocol-native continuation path: keep the live KV and * append only the tool result. Case 2 is stateless replay after restart or * branching. In thinking mode, case 2 is less faithful if the replay omits * reasoning state for the assistant call. Official Responses clients can * carry that state with reasoning items / encrypted reasoning content; when * they do not, the request is still renderable as visible history. Mark that * condition so generate_job() can prefer live / visible checkpoints and emit a * warning if it must fall back to visible replay instead of aborting the * session. */ static bool responses_validate_tool_outputs(server *s, const chat_msgs *msgs, ds4_think_mode think_mode, bool *requires_live_tool_state, bool *requires_live_reasoning, char *err, size_t errlen) { if (!msgs) return true; if (requires_live_tool_state) *requires_live_tool_state = false; if (requires_live_reasoning) *requires_live_reasoning = false; const bool needs_reasoning = ds4_think_mode_enabled(think_mode); /* Map call_id -> the nearest preceding assistant message that declares it, * built as we scan forward. This replaces a per-id backward rescan * (responses_find_prior_call_msg) that made the whole pass O(n^2) over an * attacker-supplied, uncapped message array. Only assistant messages declare * call ids (via their calls[] array), so registering them as we pass and * looking up on tool messages reproduces the nearest-preceding result. */ rax *by_call_id = raxNew(); bool ok = true; for (int i = 0; i < msgs->len && ok; i++) { const chat_msg *m = &msgs->v[i]; if (!strcmp(m->role, "assistant")) { for (int k = 0; k < m->calls.len; k++) { const char *cid = m->calls.v[k].id; if (cid && cid[0]) raxInsert(by_call_id, (unsigned char *)cid, strlen(cid), (void *)m, NULL); } continue; } if (strcmp(m->role, "tool") && strcmp(m->role, "function")) continue; stop_list ids = {0}; chat_msg_collect_tool_call_ids(m, &ids); for (int j = 0; j < ids.len; j++) { const char *id = ids.v[j]; const bool live_known = responses_live_has_call_id(s, id); void *found = id && id[0] ? raxFind(by_call_id, (unsigned char *)id, strlen(id)) : raxNotFound; const chat_msg *prior = found == raxNotFound ? NULL : (const chat_msg *)found; if (!live_known && !prior) { snprintf(err, errlen, "Responses continuation state is not available for call_id %s; retry by replaying the full input history", id); ok = false; break; } if (!prior) { if (requires_live_tool_state) *requires_live_tool_state = true; continue; } if (needs_reasoning && (!prior->reasoning || !prior->reasoning[0])) { if (requires_live_reasoning) *requires_live_reasoning = true; } } id_list_free(&ids); } raxFree(by_call_id); return ok; } /* Record the call ids and suffix candidate for a live Responses continuation. * * This only prepares evidence. generate_job() later checks that the live * server state is still exactly at the remembered token frontier before using * it. If another request already replaced the session, normal token/text/disk * prefix matching handles the request instead. */ static void responses_prepare_live_continuation(server *s, request *r, const chat_msgs *msgs) { if (!r || r->api != API_RESPONSES || !msgs || msgs->len == 0) return; int tail_start = msgs->len; while (tail_start > 0) { const chat_msg *m = &msgs->v[tail_start - 1]; if (strcmp(m->role, "tool") && strcmp(m->role, "function")) break; tail_start--; } if (tail_start == msgs->len) return; stop_list_clear(&r->responses_live_call_ids); if (tail_start > 0) { const int anchor = tail_start - 1; const chat_msg *assistant = &msgs->v[anchor]; if (strcmp(assistant->role, "assistant") || assistant->calls.len == 0) return; for (int i = 0; i < assistant->calls.len; i++) { id_list_push_unique(&r->responses_live_call_ids, assistant->calls.v[i].id); } } else { for (int i = tail_start; i < msgs->len; i++) { chat_msg_collect_tool_call_ids(&msgs->v[i], &r->responses_live_call_ids); } } if (r->responses_live_call_ids.len == 0) return; free(r->responses_live_suffix_text); r->responses_live_suffix_text = render_request_live_tool_tail(s, r, msgs, tail_start, &r->responses_live_call_ids); } static bool anthropic_msg_is_tool_result_tail(const chat_msg *m) { return m && !strcmp(m->role, "user") && ((m->tool_call_id && m->tool_call_id[0]) || m->tool_call_ids_len > 0); } /* Validate Anthropic tool results before rendering. * * A tool_result.tool_use_id is valid if it is either still bound to the live * Anthropic assistant tool-call frontier or the same request replays the prior * assistant tool_use block. The first case is the fast path: keep the sampled * KV and append only the tool-result suffix. The second case is a normal * stateless replay, where exact DSML tool memory can restore the sampled tool * bytes before prefix matching. A tool-result-only request with an unknown * live id has no safe prefix to reconstruct, so report a clear client error. */ static bool anthropic_validate_tool_results(server *s, const chat_msgs *msgs, bool *requires_live_tool_state, char *err, size_t errlen) { if (requires_live_tool_state) *requires_live_tool_state = false; if (!msgs) return true; /* Same O(1) call_id -> prior assistant message map as * responses_validate_tool_outputs, replacing the O(n^2) backward rescan. */ rax *by_call_id = raxNew(); bool ok = true; for (int i = 0; i < msgs->len && ok; i++) { const chat_msg *m = &msgs->v[i]; if (!strcmp(m->role, "assistant")) { for (int k = 0; k < m->calls.len; k++) { const char *cid = m->calls.v[k].id; if (cid && cid[0]) raxInsert(by_call_id, (unsigned char *)cid, strlen(cid), (void *)m, NULL); } continue; } if (!anthropic_msg_is_tool_result_tail(m)) continue; stop_list ids = {0}; chat_msg_collect_tool_call_ids(m, &ids); for (int j = 0; j < ids.len; j++) { const char *id = ids.v[j]; const bool live_known = anthropic_live_has_call_id(s, id); void *found = id && id[0] ? raxFind(by_call_id, (unsigned char *)id, strlen(id)) : raxNotFound; const chat_msg *prior = found == raxNotFound ? NULL : (const chat_msg *)found; if (!live_known && !prior) { snprintf(err, errlen, "Anthropic continuation state is not available for tool_use_id %s; retry by replaying the full messages history", id); ok = false; break; } if (!prior && requires_live_tool_state) { *requires_live_tool_state = true; } } id_list_free(&ids); } raxFree(by_call_id); return ok; } /* Prepare the Anthropic live-tool fast path. * * Anthropic's visible replay normally includes the assistant tool_use JSON and * the user tool_result. That replay is still only a description of what the * model sampled. If the incoming tool_result IDs match the live sampled * frontier, generate_job() can skip replay matching entirely and append just * EOS + tool_result + next assistant prefix to the real KV. */ static void anthropic_prepare_live_continuation(server *s, request *r, const chat_msgs *msgs) { if (!r || r->api != API_ANTHROPIC || !msgs || msgs->len == 0) return; int tail_end = msgs->len; while (tail_end > 0 && role_is_system(msgs->v[tail_end - 1].role)) tail_end--; int tail_start = tail_end; while (tail_start > 0 && anthropic_msg_is_tool_result_tail(&msgs->v[tail_start - 1])) { tail_start--; } if (tail_start == tail_end) return; stop_list_clear(&r->anthropic_live_call_ids); for (int i = tail_start; i < msgs->len; i++) { chat_msg_collect_tool_call_ids(&msgs->v[i], &r->anthropic_live_call_ids); } if (r->anthropic_live_call_ids.len == 0) return; free(r->anthropic_live_suffix_text); r->anthropic_live_suffix_text = render_request_live_tool_tail(s, r, msgs, tail_start, &r->anthropic_live_call_ids); } /* The API parsers are intentionally selective JSON parsers: they keep only * fields that affect model semantics, rendering, streaming, or cache keys, and * skip extension fields. The output is always a rendered DS4 chat/completion * prompt plus the small amount of protocol state needed to translate the reply. */ static bool parse_chat_request(ds4_engine *e, server *s, const char *body, int def_tokens, int ctx_size, request *r, char *err, size_t errlen) { request_init(r, REQ_CHAT, def_tokens); r->model_syntax = server_model_syntax_for_engine(e); const char *p = body; bool got_messages = false; bool tool_choice_none = false; bool got_thinking = false; bool thinking_enabled = true; ds4_think_mode reasoning_effort = DS4_THINK_HIGH; chat_msgs msgs = {0}; char *tool_schemas = NULL; json_ws(&p); if (*p != '{') goto bad; p++; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto bad; json_ws(&p); if (*p != ':') { free(key); goto bad; } p++; if (!strcmp(key, "messages")) { chat_msgs_free(&msgs); if (!parse_messages(&p, &msgs)) { free(key); goto bad; } got_messages = true; } else if (!strcmp(key, "tools")) { free(tool_schemas); tool_schemas = NULL; if (!parse_tools_value(&p, &tool_schemas, &r->tool_orders)) { free(key); goto bad; } } else if (!strcmp(key, "tool_choice")) { json_ws(&p); if (*p == '"') { char *choice = NULL; if (!json_string(&p, &choice)) { free(key); goto bad; } tool_choice_none = !strcmp(choice, "none"); free(choice); } else if (!json_skip_value(&p)) { free(key); goto bad; } } else if (!strcmp(key, "model")) { if (!json_string_replace(&p, &r->model)) { free(key); goto bad; } r->model_from_request = true; } else if (!strcmp(key, "max_tokens") || !strcmp(key, "max_completion_tokens")) { if (!json_int(&p, &r->max_tokens)) { free(key); goto bad; } } else if (!strcmp(key, "temperature")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->temperature = (float)v; r->temperature_set = true; } else if (!strcmp(key, "top_p")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->top_p = (float)v; r->top_p_set = true; } else if (!strcmp(key, "min_p")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->min_p = (float)v; r->min_p_set = true; } else if (!strcmp(key, "top_k")) { if (!json_int(&p, &r->top_k)) { free(key); goto bad; } r->top_k_set = true; } else if (!strcmp(key, "seed")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->seed = v > 0.0 ? (uint64_t)v : 0; } else if (!strcmp(key, "stream")) { if (!json_bool(&p, &r->stream)) { free(key); goto bad; } } else if (!strcmp(key, "stream_options")) { if (!parse_stream_options(&p, &r->stream_include_usage)) { free(key); goto bad; } } else if (!strcmp(key, "ignore_eos")) { if (!parse_ignore_eos_value(&p, r)) { free(key); goto bad; } } else if (!strcmp(key, "thinking")) { if (!parse_thinking_control_value(&p, &thinking_enabled)) { free(key); goto bad; } got_thinking = true; } else if (!strcmp(key, "reasoning_effort")) { if (!parse_reasoning_effort_value(&p, &reasoning_effort)) { free(key); goto bad; } } else if (!strcmp(key, "chat_template_kwargs")) { if (!parse_chat_template_kwargs(&p, &thinking_enabled, &got_thinking, &reasoning_effort)) { free(key); goto bad; } } else if (!strcmp(key, "think")) { if (!json_bool(&p, &thinking_enabled)) { free(key); goto bad; } got_thinking = true; } else if (!strcmp(key, "stop")) { if (!parse_stop(&p, &r->stops)) { free(key); goto bad; } } else if (!json_skip_value(&p)) { free(key); goto bad; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (*p != '}') goto bad; if (!got_messages) { snprintf(err, errlen, "missing messages"); chat_msgs_free(&msgs); free(tool_schemas); request_free(r); return false; } if (!request_validate_ignore_eos(r, err, errlen)) { chat_msgs_free(&msgs); free(tool_schemas); request_free(r); return false; } r->has_tools = tool_schemas && tool_schemas[0] && !tool_choice_none; if (!got_thinking && model_alias_disables_thinking(r->model)) thinking_enabled = false; if (!got_thinking && model_alias_enables_thinking(r->model)) thinking_enabled = true; r->think_mode = ds4_think_mode_for_context( think_mode_from_enabled(thinking_enabled, reasoning_effort), ctx_size); kv_cache_restore_tool_memory_for_messages(s, &msgs); tool_memory_attach_to_messages(s, &msgs, &r->tool_replay); const char *active_tool_schemas = r->has_tools ? tool_schemas : NULL; r->prompt_preserves_reasoning = chat_history_uses_tool_context(&msgs, active_tool_schemas); r->prompt_text = render_chat_prompt_text_for_syntax( r->model_syntax, &msgs, active_tool_schemas, &r->tool_orders, r->think_mode); if (!request_tokenize_multimodal_prompt(e, s, r, &msgs, err, errlen)) { chat_msgs_free(&msgs); free(tool_schemas); request_free(r); return false; } chat_msgs_free(&msgs); free(tool_schemas); return true; bad: chat_msgs_free(&msgs); free(tool_schemas); snprintf(err, errlen, "invalid JSON request"); request_free(r); return false; } static bool parse_anthropic_request(ds4_engine *e, server *s, const char *body, int def_tokens, int ctx_size, request *r, char *err, size_t errlen) { request_init(r, REQ_CHAT, def_tokens); r->api = API_ANTHROPIC; r->model_syntax = server_model_syntax_for_engine(e); const char *p = body; bool got_messages = false; bool tool_choice_none = false; bool got_thinking = false; bool thinking_enabled = true; ds4_think_mode reasoning_effort = DS4_THINK_HIGH; chat_msgs msgs = {0}; char *system = NULL; char *tool_schemas = NULL; json_ws(&p); if (*p != '{') goto bad; p++; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto bad; json_ws(&p); if (*p != ':') { free(key); goto bad; } p++; if (!strcmp(key, "messages")) { chat_msgs_free(&msgs); if (!parse_anthropic_messages(&p, &msgs)) { free(key); goto bad; } got_messages = true; } else if (!strcmp(key, "system")) { char *tmp = NULL; if (!parse_anthropic_system(&p, &tmp)) { free(key); goto bad; } free(system); system = tmp; } else if (!strcmp(key, "tools")) { free(tool_schemas); tool_schemas = NULL; if (!parse_tools_value(&p, &tool_schemas, &r->tool_orders)) { free(key); goto bad; } } else if (!strcmp(key, "tool_choice")) { json_ws(&p); if (*p == '{') { p++; json_ws(&p); while (*p && *p != '}') { char *ckey = NULL; if (!json_string(&p, &ckey)) { free(key); goto bad; } json_ws(&p); if (*p != ':') { free(ckey); free(key); goto bad; } p++; if (!strcmp(ckey, "type")) { char *choice = NULL; if (!json_string(&p, &choice)) { free(ckey); free(key); goto bad; } tool_choice_none = !strcmp(choice, "none"); free(choice); } else if (!json_skip_value(&p)) { free(ckey); free(key); goto bad; } free(ckey); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (*p != '}') { free(key); goto bad; } p++; } else if (!json_skip_value(&p)) { free(key); goto bad; } } else if (!strcmp(key, "model")) { if (!json_string_replace(&p, &r->model)) { free(key); goto bad; } r->model_from_request = true; } else if (!strcmp(key, "max_tokens")) { if (!json_int(&p, &r->max_tokens)) { free(key); goto bad; } } else if (!strcmp(key, "temperature")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->temperature = (float)v; r->temperature_set = true; } else if (!strcmp(key, "top_p")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->top_p = (float)v; r->top_p_set = true; } else if (!strcmp(key, "top_k")) { if (!json_int(&p, &r->top_k)) { free(key); goto bad; } r->top_k_set = true; } else if (!strcmp(key, "stream")) { if (!json_bool(&p, &r->stream)) { free(key); goto bad; } } else if (!strcmp(key, "stop_sequences")) { if (!parse_stop(&p, &r->stops)) { free(key); goto bad; } } else if (!strcmp(key, "thinking")) { if (!parse_thinking_control_value(&p, &thinking_enabled)) { free(key); goto bad; } got_thinking = true; } else if (!strcmp(key, "output_config")) { if (!parse_output_config_effort(&p, &reasoning_effort)) { free(key); goto bad; } } else if (!strcmp(key, "reasoning_effort")) { if (!parse_reasoning_effort_value(&p, &reasoning_effort)) { free(key); goto bad; } } else if (!json_skip_value(&p)) { free(key); goto bad; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (*p != '}') goto bad; if (!got_messages) { snprintf(err, errlen, "missing messages"); chat_msgs_free(&msgs); free(system); free(tool_schemas); request_free(r); return false; } if (system && system[0]) { chat_msg msg = {0}; msg.role = xstrdup("system"); msg.content = system; system = NULL; chat_msgs_push(&msgs, msg); if (r->model_syntax == SERVER_MODEL_SYNTAX_DEEPSEEK41) { /* This is the API's initial system prompt, not a later system * turn, which V4.1 preserves at its original history position. */ memmove(msgs.v + 1, msgs.v, (size_t)(msgs.len - 1) * sizeof(msgs.v[0])); msgs.v[0] = msg; } } r->has_tools = tool_schemas && tool_schemas[0] && !tool_choice_none; if (!got_thinking && model_alias_disables_thinking(r->model)) thinking_enabled = false; if (!got_thinking && model_alias_enables_thinking(r->model)) thinking_enabled = true; r->think_mode = ds4_think_mode_for_context( think_mode_from_enabled(thinking_enabled, reasoning_effort), ctx_size); if (!anthropic_validate_tool_results(s, &msgs, &r->anthropic_requires_live_tool_state, err, errlen)) { chat_msgs_free(&msgs); free(system); free(tool_schemas); request_free(r); return false; } kv_cache_restore_tool_memory_for_messages(s, &msgs); tool_memory_attach_to_messages(s, &msgs, &r->tool_replay); anthropic_prepare_live_continuation(s, r, &msgs); const char *active_tool_schemas = r->has_tools ? tool_schemas : NULL; r->prompt_preserves_reasoning = chat_history_uses_tool_context(&msgs, active_tool_schemas); r->prompt_text = render_chat_prompt_text_for_syntax( r->model_syntax, &msgs, active_tool_schemas, &r->tool_orders, r->think_mode); if (!request_tokenize_multimodal_prompt(e, s, r, &msgs, err, errlen)) { chat_msgs_free(&msgs); free(system); free(tool_schemas); request_free(r); return false; } chat_msgs_free(&msgs); free(system); free(tool_schemas); return true; bad: chat_msgs_free(&msgs); free(system); free(tool_schemas); snprintf(err, errlen, "invalid JSON request"); request_free(r); return false; } /* Responses API: convert a content-array item (input_text/output_text/text) into a * concatenated string. Strict shape check: bare string, null, or an array of * recognized text blocks. Numbers / objects / arrays-of-primitives at the top * level all reject so the client sees a 400 instead of an answer built on * silently dropped context. */ static bool parse_responses_content_array(const char **p, char **out, server_image_inputs *images) { json_ws(p); if (**p == '"') return json_string(p, out); if (json_lit(p, "null")) { *out = xstrdup(""); return true; } if (**p != '[') { return false; } (*p)++; buf b = {0}; json_ws(p); while (**p && **p != ']') { if (**p == '"') { char *s = NULL; if (!json_string(p, &s)) goto fail; buf_puts(&b, s); free(s); } else if (**p == '{') { (*p)++; char *type = NULL; char *text = NULL; char *image_url = NULL; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) { free(type); free(text); free(image_url); goto fail; } json_ws(p); if (**p != ':') { free(key); free(type); free(text); free(image_url); goto fail; } (*p)++; if (!strcmp(key, "type")) { if (!json_string_replace(p, &type)) { free(key); free(type); free(text); free(image_url); goto fail; } } else if (!strcmp(key, "text")) { /* The text field of a typed content block is a plain JSON * string. Accept null as the empty string for parity with * upstream serializers that emit null for empty blocks. */ json_ws(p); if (json_lit(p, "null")) { free(text); text = xstrdup(""); } else if (!json_string_replace(p, &text)) { free(key); free(type); free(text); free(image_url); goto fail; } } else if (!strcmp(key, "image_url")) { free(image_url); image_url = NULL; if (!parse_image_url_value(p, &image_url)) { free(key); free(type); free(text); goto fail; } } else if (!json_skip_value(p)) { free(key); free(type); free(text); free(image_url); goto fail; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') { free(type); free(text); free(image_url); goto fail; } (*p)++; /* Fail closed: a content object must carry a known text-like type * AND a text field. Anything else — missing type, missing text, * image/file/audio types, future schema-drift — is rejected so the * client gets a 400 instead of an answer built on context the * server discarded silently. */ bool is_text_block = type && ( !strcmp(type, "input_text") || !strcmp(type, "output_text") || !strcmp(type, "text") || !strcmp(type, "summary_text") || !strcmp(type, "reasoning_text")); bool is_image_block = type && !strcmp(type, "input_image"); if (is_image_block) { char marker[SERVER_IMAGE_MARKER_BYTES]; if (!images || !server_image_inputs_push_data_uri(images, image_url, marker)) { free(type); free(text); free(image_url); goto fail; } buf_puts(&b, marker); } else if (!is_text_block || !text) { free(type); free(text); free(image_url); goto fail; } else { buf_puts(&b, text); } free(type); free(text); free(image_url); } else { /* Reject primitives, arrays-of-arrays, nulls: a content array * element must be either a string or a typed text object. */ goto fail; } json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') goto fail; (*p)++; *out = buf_take(&b); return true; fail: buf_free(&b); return false; } static bool parse_responses_content_array_replace(const char **p, char **dst) { char *tmp = NULL; if (!parse_responses_content_array(p, &tmp, NULL)) return false; free(*dst); *dst = tmp; return true; } static bool parse_responses_content_array_multimodal( const char **p, char **dst, server_image_inputs *images) { char *tmp = NULL; server_image_inputs_free(images); if (!parse_responses_content_array(p, &tmp, images)) { server_image_inputs_free(images); return false; } free(*dst); *dst = tmp; return true; } /* Codex /v1/responses input items have a `type` discriminator (message, * function_call, function_call_output, reasoning, custom_tool_call, * custom_tool_call_output, ...). We collapse them into chat_msgs the same way * the chat completion / Anthropic parsers do, so the rest of the engine sees a * single conversation history shape. * * Protocol contract for stateless replay: * - The client must replay response.output items before tool outputs. * - For reasoning models, the replay must also include reasoning state. DS4 * can render plain reasoning summaries/content, but it cannot decrypt * reasoning.encrypted_content. If live state is unavailable and the replay * only contains visible messages/tool calls, later validation marks it as a * lower-fidelity replay; generate_job() logs that and continues from the * visible transcript rather than killing a recoverable agent session. * * Reasoning items are merged into the next assistant message so * render_chat_prompt_text can wrap them in . */ static bool parse_responses_input(const char **p, chat_msgs *msgs, buf *loaded_tool_schemas, tool_schema_orders *orders) { json_ws(p); if (**p != '[') return false; (*p)++; buf pending_reasoning = {0}; json_ws(p); while (**p && **p != ']') { if (**p != '{') goto fail; (*p)++; char *type = NULL; char *role = NULL; char *content = NULL; char *name = NULL; char *namespace = NULL; char *call_id = NULL; char *item_id = NULL; char *arguments = NULL; char *output = NULL; char *input_str = NULL; char *summary = NULL; char *action = NULL; char *result = NULL; char *tools_json = NULL; char *status_str = NULL; server_image_inputs content_images = {0}, output_images = {0}; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) goto item_fail; json_ws(p); if (**p != ':') { free(key); goto item_fail; } (*p)++; if (!strcmp(key, "type")) { if (!json_string_replace(p, &type)) { free(key); goto item_fail; } } else if (!strcmp(key, "role")) { if (!json_string_replace(p, &role)) { free(key); goto item_fail; } } else if (!strcmp(key, "content")) { if (!parse_responses_content_array_multimodal( p, &content, &content_images)) { free(key); goto item_fail; } } else if (!strcmp(key, "name")) { if (!json_string_replace(p, &name)) { free(key); goto item_fail; } } else if (!strcmp(key, "namespace")) { if (!json_string_replace(p, &namespace)) { free(key); goto item_fail; } } else if (!strcmp(key, "call_id")) { if (!json_string_replace(p, &call_id)) { free(key); goto item_fail; } } else if (!strcmp(key, "id")) { if (!json_string_replace(p, &item_id)) { free(key); goto item_fail; } } else if (!strcmp(key, "arguments")) { json_ws(p); if (**p == '"') { if (!json_string_replace(p, &arguments)) { free(key); goto item_fail; } } else if (!json_raw_value_replace(p, &arguments)) { free(key); goto item_fail; } } else if (!strcmp(key, "output")) { server_image_inputs_free(&output_images); json_ws(p); if (**p == '[') { if (!parse_responses_content_array_multimodal(p, &output, &output_images)) { free(key); goto item_fail; } } else if (**p == '"') { if (!json_string_replace(p, &output)) { free(key); goto item_fail; } } else if (!json_raw_value_replace(p, &output)) { free(key); goto item_fail; } } else if (!strcmp(key, "input")) { json_ws(p); if (**p == '"') { if (!json_string_replace(p, &input_str)) { free(key); goto item_fail; } } else if (!json_raw_value_replace(p, &input_str)) { free(key); goto item_fail; } } else if (!strcmp(key, "summary")) { if (!parse_responses_content_array_replace(p, &summary)) { free(key); goto item_fail; } } else if (!strcmp(key, "action")) { if (!json_raw_value_replace(p, &action)) { free(key); goto item_fail; } } else if (!strcmp(key, "result")) { json_ws(p); if (**p == '"') { if (!json_string_replace(p, &result)) { free(key); goto item_fail; } } else if (!json_raw_value_replace(p, &result)) { free(key); goto item_fail; } } else if (!strcmp(key, "status")) { if (!json_string_replace(p, &status_str)) { free(key); goto item_fail; } } else if (!strcmp(key, "tools")) { /* tool_search_output items carry their discovered tool list * here instead of in `output` / `result`. Keep it separate * from the human-visible result body so malformed tool lists * never get mistaken for normal tool output. */ if (!json_raw_value_replace(p, &tools_json)) { free(key); goto item_fail; } } else if (!json_skip_value(p)) { free(key); goto item_fail; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); continue; item_fail: free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); buf_free(&pending_reasoning); return false; } if (**p != '}') { free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); goto fail; } (*p)++; const char *t = type ? type : "message"; /* Replayed items must be in a terminal "completed" state. in_progress, * incomplete, and failed all represent partial model state the client * never confirmed — feeding them back as history would let DS4 continue * from a tool action that never finished. Reject explicitly. */ if (status_str && status_str[0] && strcmp(status_str, "completed") != 0) { free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); buf_free(&pending_reasoning); return false; } if ((content_images.len && (strcmp(t, "message") || (role && strcmp(role, "user")))) || (output_images.len && strcmp(t, "function_call_output") && strcmp(t, "custom_tool_call_output"))) { free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); buf_free(&pending_reasoning); return false; } /* Three classes of items: * 1. consumes_reasoning: assistant message / function_call / hosted-tool * call. Attaches pending reasoning to its own assistant message. * 2. is_bookkeeping: compaction / context_compaction etc. Semantically * transparent — passes through without touching pending_reasoning. * 3. everything else (user message, tool output): forces pending * reasoning to flush in-position as an empty assistant message so it * stays before this item in the rendered history. */ bool consumes_reasoning = (!strcmp(t, "message") && role && !strcmp(role, "assistant")) || !strcmp(t, "function_call") || !strcmp(t, "custom_tool_call") || !strcmp(t, "local_shell_call") || !strcmp(t, "web_search_call") || !strcmp(t, "tool_search_call") || !strcmp(t, "image_generation_call"); bool is_bookkeeping = !strcmp(t, "compaction") || !strcmp(t, "context_compaction"); if (!consumes_reasoning && !is_bookkeeping && pending_reasoning.len) { chat_msg flush_msg = {0}; flush_msg.role = xstrdup("assistant"); flush_msg.content = xstrdup(""); flush_msg.reasoning = buf_take(&pending_reasoning); chat_msgs_push(msgs, flush_msg); } if (!strcmp(t, "message")) { chat_msg msg = {0}; msg.role = xstrdup(role ? role : "user"); msg.content = content ? content : xstrdup(""); content = NULL; msg.images = content_images; memset(&content_images, 0, sizeof(content_images)); if (!strcmp(msg.role, "assistant") && pending_reasoning.len) { msg.reasoning = buf_take(&pending_reasoning); } chat_msgs_push(msgs, msg); } else if (!strcmp(t, "function_call") || !strcmp(t, "custom_tool_call")) { tool_call tc = {0}; tc.id = xstrdup(call_id ? call_id : item_id ? item_id : ""); /* function_call uses `arguments` (JSON string); custom_tool_call uses * `input` (free text). Treat both as the same on-wire argument blob — * append_dsml_arguments_from_json will fall back to a single text param * if the value isn't a JSON object. */ const char *args_src = arguments ? arguments : input_str ? input_str : "{}"; tc.arguments = xstrdup(args_src); if (strcmp(t, "custom_tool_call") && namespace && namespace[0] && name && name[0]) { buf qualified = {0}; buf_puts(&qualified, namespace); buf_puts(&qualified, name); tc.name = buf_take(&qualified); } else { tc.name = xstrdup(name ? name : ""); } /* A Responses turn that has both message text and tool calls splits * them across separate output items; the chat template renders the * second assistant record without an `<|Assistant|>` prefix, leaving * the tool call bare. Merge into the previous assistant message * when nothing user-like / tool-output-like came between them. */ chat_msg *last = msgs->len ? &msgs->v[msgs->len - 1] : NULL; if (last && !strcmp(last->role, "assistant")) { if (pending_reasoning.len && (!last->reasoning || !last->reasoning[0])) { free(last->reasoning); last->reasoning = buf_take(&pending_reasoning); } tool_calls_push(&last->calls, tc); } else { chat_msg msg = {0}; msg.role = xstrdup("assistant"); msg.content = xstrdup(""); if (pending_reasoning.len) msg.reasoning = buf_take(&pending_reasoning); tool_calls_push(&msg.calls, tc); chat_msgs_push(msgs, msg); } } else if (!strcmp(t, "function_call_output") || !strcmp(t, "custom_tool_call_output")) { chat_msg msg = {0}; msg.role = xstrdup("tool"); msg.content = output ? output : xstrdup(""); output = NULL; msg.images = output_images; memset(&output_images, 0, sizeof(output_images)); if (call_id || item_id) { chat_msg_add_tool_call_id(&msg, call_id ? call_id : item_id); } chat_msgs_push(msgs, msg); } else if (!strcmp(t, "reasoning")) { /* Stash so it merges into the next assistant message. summary is the * short-form list, content is the verbose chain. Either can be empty. */ if (summary && summary[0]) { if (pending_reasoning.len) buf_putc(&pending_reasoning, '\n'); buf_puts(&pending_reasoning, summary); } if (content && content[0]) { if (pending_reasoning.len) buf_putc(&pending_reasoning, '\n'); buf_puts(&pending_reasoning, content); } } else if (!strcmp(t, "local_shell_call") || !strcmp(t, "web_search_call") || !strcmp(t, "tool_search_call") || !strcmp(t, "image_generation_call")) { /* Hosted-tool history isn't natively supported (DS4 doesn't register * these tools), but a Codex client may still replay them when the * model used them in a prior turn. Surface them as function_call * shaped history so the next prompt retains the action that ran. */ tool_call tc = {0}; tc.id = xstrdup(call_id ? call_id : item_id ? item_id : ""); if (!strcmp(t, "tool_search_call")) { tc.name = xstrdup("tool_search"); } else if (!strcmp(t, "local_shell_call")) { tc.name = xstrdup("local_shell"); } else { tc.name = xstrdup(t); } const char *args_src = action ? action : arguments ? arguments : input_str ? input_str : "{}"; tc.arguments = xstrdup(args_src); chat_msg *last = msgs->len ? &msgs->v[msgs->len - 1] : NULL; if (last && !strcmp(last->role, "assistant")) { if (pending_reasoning.len && (!last->reasoning || !last->reasoning[0])) { free(last->reasoning); last->reasoning = buf_take(&pending_reasoning); } tool_calls_push(&last->calls, tc); } else { chat_msg msg = {0}; msg.role = xstrdup("assistant"); msg.content = xstrdup(""); if (pending_reasoning.len) msg.reasoning = buf_take(&pending_reasoning); tool_calls_push(&msg.calls, tc); chat_msgs_push(msgs, msg); } } else if (!strcmp(t, "local_shell_call_output") || !strcmp(t, "web_search_call_output") || !strcmp(t, "tool_search_output") || !strcmp(t, "tool_search_call_output") || !strcmp(t, "image_generation_call_output")) { if (!strcmp(t, "tool_search_output") && tools_json && loaded_tool_schemas && orders) { const char *tools_p = tools_json; char *schemas = NULL; if (!parse_tools_value(&tools_p, &schemas, orders)) { free(schemas); free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); buf_free(&pending_reasoning); return false; } if (schemas && schemas[0]) { if (loaded_tool_schemas->len) buf_putc(loaded_tool_schemas, '\n'); buf_puts(loaded_tool_schemas, schemas); } free(schemas); } chat_msg msg = {0}; msg.role = xstrdup("tool"); const char *body = output ? output : result ? result : tools_json ? tools_json : ""; msg.content = xstrdup(body); if (call_id || item_id) { chat_msg_add_tool_call_id(&msg, call_id ? call_id : item_id); } chat_msgs_push(msgs, msg); } else if (!is_bookkeeping) { /* Anything we don't have an explicit branch for would silently * drop replay context. Fail the parse instead so the client sees * the limitation rather than ending up with stale generation * built on an incomplete history. Only compaction/context_compaction * (true Codex bookkeeping) are allowed to pass through silently. */ free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); buf_free(&pending_reasoning); return false; } free(type); free(role); free(content); free(name); free(namespace); free(call_id); free(item_id); free(arguments); free(output); free(input_str); free(summary); free(action); free(result); free(tools_json); free(status_str); server_image_inputs_free(&content_images); server_image_inputs_free(&output_images); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != ']') goto fail; (*p)++; /* Trailing reasoning with no following message/tool item: attach it to an * empty assistant message so the next turn still renders a ... * block. Dropping it loses model state when a previous response ended with * a reasoning-only incomplete turn and the client replays the history. */ if (pending_reasoning.len) { chat_msg msg = {0}; msg.role = xstrdup("assistant"); msg.content = xstrdup(""); msg.reasoning = buf_take(&pending_reasoning); chat_msgs_push(msgs, msg); } buf_free(&pending_reasoning); return true; fail: buf_free(&pending_reasoning); return false; } /* Responses API has `reasoning: {"effort": "...", "summary": "..."}`. effort * controls thinking depth; summary mode (auto/concise/detailed) controls * whether the wire emits summary deltas at all — per the spec, no reasoning * summary is surfaced unless the client opts in. */ static bool parse_responses_reasoning(const char **p, ds4_think_mode *effort, bool *summary_opted_in, bool *effort_seen) { json_ws(p); if (json_lit(p, "null")) return true; if (**p != '{') return json_skip_value(p); (*p)++; json_ws(p); while (**p && **p != '}') { char *key = NULL; if (!json_string(p, &key)) return false; json_ws(p); if (**p != ':') { free(key); return false; } (*p)++; if (!strcmp(key, "effort")) { json_ws(p); /* A `null` effort doesn't change thinking_enabled — it's the same * as omitting the field. Only treat the field as a control if it * carried an actual value. */ if (json_lit(p, "null")) { /* nothing */ } else { if (!parse_reasoning_effort_value(p, effort)) { free(key); return false; } if (effort_seen) *effort_seen = true; } } else if (!strcmp(key, "summary")) { json_ws(p); if (json_lit(p, "null")) { /* explicit null disables summary */ } else if (**p == '"') { char *mode = NULL; if (!json_string(p, &mode)) { free(key); return false; } if (summary_opted_in && (!strcmp(mode, "auto") || !strcmp(mode, "concise") || !strcmp(mode, "detailed"))) { *summary_opted_in = true; } free(mode); } else if (!json_skip_value(p)) { free(key); return false; } } else if (!json_skip_value(p)) { free(key); return false; } free(key); json_ws(p); if (**p == ',') (*p)++; json_ws(p); } if (**p != '}') return false; (*p)++; return true; } static bool parse_responses_request(ds4_engine *e, server *s, const char *body, int def_tokens, int ctx_size, request *r, char *err, size_t errlen) { request_init(r, REQ_CHAT, def_tokens); r->api = API_RESPONSES; r->model_syntax = server_model_syntax_for_engine(e); const char *p = body; bool got_input = false; bool tool_choice_none = false; bool got_thinking = false; bool thinking_enabled = true; ds4_think_mode reasoning_effort = DS4_THINK_HIGH; chat_msgs msgs = {0}; buf loaded_tool_schemas = {0}; char *instructions = NULL; char *tool_schemas = NULL; json_ws(&p); if (*p != '{') goto bad; p++; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto bad; json_ws(&p); if (*p != ':') { free(key); goto bad; } p++; if (!strcmp(key, "input")) { chat_msgs_free(&msgs); json_ws(&p); /* Codex CLI always sends `input` as an array; tolerate bare strings * for parity with other Responses-API callers. */ if (*p == '"') { char *plain = NULL; if (!json_string(&p, &plain)) { free(key); goto bad; } chat_msg msg = {0}; msg.role = xstrdup("user"); msg.content = plain; chat_msgs_push(&msgs, msg); } else if (!parse_responses_input(&p, &msgs, &loaded_tool_schemas, &r->tool_orders)) { free(key); goto bad; } got_input = true; } else if (!strcmp(key, "instructions")) { free(instructions); instructions = NULL; json_ws(&p); if (json_lit(&p, "null")) { instructions = xstrdup(""); } else if (!json_string(&p, &instructions)) { free(key); goto bad; } } else if (!strcmp(key, "tools")) { free(tool_schemas); tool_schemas = NULL; if (!parse_tools_value(&p, &tool_schemas, &r->tool_orders)) { free(key); goto bad; } } else if (!strcmp(key, "tool_choice")) { json_ws(&p); if (*p == '"') { char *choice = NULL; if (!json_string(&p, &choice)) { free(key); goto bad; } /* DS4 honours "none" (disable tools) and "auto" (model decides). * "required" and explicit function targets need constrained * decoding we don't implement — reject so clients see the * limitation instead of silently downgrading to auto. */ if (!strcmp(choice, "none")) { tool_choice_none = true; } else if (strcmp(choice, "auto") != 0) { snprintf(err, errlen, "tool_choice=%s not supported", choice); free(choice); free(key); chat_msgs_free(&msgs); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); request_free(r); return false; } free(choice); } else if (*p == '{') { snprintf(err, errlen, "forced tool_choice not supported"); free(key); chat_msgs_free(&msgs); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); request_free(r); return false; } else if (!json_skip_value(&p)) { free(key); goto bad; } } else if (!strcmp(key, "model")) { if (!json_string_replace(&p, &r->model)) { free(key); goto bad; } r->model_from_request = true; } else if (!strcmp(key, "max_output_tokens") || !strcmp(key, "max_tokens")) { if (!json_int(&p, &r->max_tokens)) { free(key); goto bad; } } else if (!strcmp(key, "temperature")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->temperature = (float)v; r->temperature_set = true; } else if (!strcmp(key, "top_p")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->top_p = (float)v; r->top_p_set = true; } else if (!strcmp(key, "stream")) { if (!json_bool(&p, &r->stream)) { free(key); goto bad; } } else if (!strcmp(key, "reasoning")) { bool effort_seen = false; if (!parse_responses_reasoning(&p, &reasoning_effort, &r->reasoning_summary_emit, &effort_seen)) { free(key); goto bad; } /* Only an explicit effort value counts as the client opting into * thinking control. summary alone, or `reasoning: null`, leaves the * default behaviour (and the model_alias_* fallbacks below) intact. */ if (effort_seen) { got_thinking = true; /* Responses-API effort "none" disables thinking; other values * pass through as the think mode. */ if (reasoning_effort == DS4_THINK_NONE) thinking_enabled = false; } } else if (!strcmp(key, "previous_response_id") || !strcmp(key, "conversation")) { /* Official Responses state can be durable: * previous_response_id chains to a stored prior response, and * conversation points at a persistent Conversations object. * * DS4 does not yet implement that durable store. The supported * modes are either (a) a live in-memory continuation checked by * visible transcript / tool call ids, or (b) stateless replay of * the full input items. Accepting a non-null durable reference * without loading the referenced items would silently truncate the * prompt, so reject it explicitly. */ json_ws(&p); if (!json_lit(&p, "null")) { snprintf(err, errlen, "%s is not supported; replay full input instead", key); free(key); chat_msgs_free(&msgs); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); request_free(r); return false; } } else if (!json_skip_value(&p)) { free(key); goto bad; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (*p != '}') goto bad; if (!got_input) { snprintf(err, errlen, "missing input"); chat_msgs_free(&msgs); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); request_free(r); return false; } /* instructions in the Responses API replaces any system message — for Codex * it carries the full agent system prompt. Prepend it so render produces a * standard system+chat layout. */ if (instructions && instructions[0]) { chat_msg msg = {0}; msg.role = xstrdup("system"); msg.content = instructions; instructions = NULL; /* Insert at the head so it precedes the conversation. */ chat_msgs_push(&msgs, msg); if (msgs.len > 1) { chat_msg tmp = msgs.v[msgs.len - 1]; for (int i = msgs.len - 1; i > 0; i--) msgs.v[i] = msgs.v[i - 1]; msgs.v[0] = tmp; } } buf combined_tool_schemas = {0}; if (tool_schemas && tool_schemas[0]) buf_puts(&combined_tool_schemas, tool_schemas); if (loaded_tool_schemas.len) { if (combined_tool_schemas.len) buf_putc(&combined_tool_schemas, '\n'); buf_append(&combined_tool_schemas, loaded_tool_schemas.ptr, loaded_tool_schemas.len); } const char *active_tool_schemas = (!tool_choice_none && combined_tool_schemas.len) ? combined_tool_schemas.ptr : NULL; r->has_tools = active_tool_schemas && active_tool_schemas[0]; if (!got_thinking && model_alias_disables_thinking(r->model)) thinking_enabled = false; if (!got_thinking && model_alias_enables_thinking(r->model)) thinking_enabled = true; r->think_mode = ds4_think_mode_for_context( think_mode_from_enabled(thinking_enabled, reasoning_effort), ctx_size); if (!responses_validate_tool_outputs(s, &msgs, r->think_mode, &r->responses_requires_live_tool_state, &r->responses_requires_live_reasoning, err, errlen)) { chat_msgs_free(&msgs); buf_free(&combined_tool_schemas); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); request_free(r); return false; } kv_cache_restore_tool_memory_for_messages(s, &msgs); tool_memory_attach_to_messages(s, &msgs, &r->tool_replay); r->prompt_preserves_reasoning = chat_history_uses_tool_context(&msgs, active_tool_schemas); responses_prepare_live_continuation(s, r, &msgs); r->prompt_text = render_chat_prompt_text_for_syntax( r->model_syntax, &msgs, active_tool_schemas, &r->tool_orders, r->think_mode); if (!request_tokenize_multimodal_prompt(e, s, r, &msgs, err, errlen)) { chat_msgs_free(&msgs); buf_free(&combined_tool_schemas); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); request_free(r); return false; } chat_msgs_free(&msgs); buf_free(&combined_tool_schemas); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); return true; bad: chat_msgs_free(&msgs); buf_free(&loaded_tool_schemas); free(instructions); free(tool_schemas); snprintf(err, errlen, "invalid JSON request"); request_free(r); return false; } static bool parse_prompt(const char **p, char **out) { json_ws(p); if (**p == '"') return json_string(p, out); if (**p != '[') { if (!json_skip_value(p)) return false; *out = xstrdup(""); return true; } (*p)++; json_ws(p); if (**p == '"') { if (!json_string(p, out)) return false; } else { *out = xstrdup(""); if (**p && **p != ']' && !json_skip_value(p)) return false; } while (**p && **p != ']') { json_ws(p); if (**p == ',') { (*p)++; if (!json_skip_value(p)) return false; } else { break; } } if (**p != ']') return false; (*p)++; return true; } static bool parse_completion_request(ds4_engine *e, const char *body, int def_tokens, int ctx_size, request *r, char *err, size_t errlen) { request_init(r, REQ_COMPLETION, def_tokens); r->model_syntax = server_model_syntax_for_engine(e); const char *p = body; char *prompt = NULL; bool got_thinking = false; bool thinking_enabled = true; ds4_think_mode reasoning_effort = DS4_THINK_HIGH; json_ws(&p); if (*p != '{') goto bad; p++; json_ws(&p); while (*p && *p != '}') { char *key = NULL; if (!json_string(&p, &key)) goto bad; json_ws(&p); if (*p != ':') { free(key); goto bad; } p++; if (!strcmp(key, "prompt")) { char *tmp = NULL; if (!parse_prompt(&p, &tmp)) { free(tmp); free(key); goto bad; } free(prompt); prompt = tmp; } else if (!strcmp(key, "model")) { if (!json_string_replace(&p, &r->model)) { free(key); goto bad; } r->model_from_request = true; } else if (!strcmp(key, "max_tokens")) { if (!json_int(&p, &r->max_tokens)) { free(key); goto bad; } } else if (!strcmp(key, "temperature")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->temperature = (float)v; r->temperature_set = true; } else if (!strcmp(key, "top_p")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->top_p = (float)v; r->top_p_set = true; } else if (!strcmp(key, "min_p")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->min_p = (float)v; r->min_p_set = true; } else if (!strcmp(key, "top_k")) { if (!json_int(&p, &r->top_k)) { free(key); goto bad; } r->top_k_set = true; } else if (!strcmp(key, "seed")) { double v = 0.0; if (!json_number(&p, &v)) { free(key); goto bad; } r->seed = v > 0.0 ? (uint64_t)v : 0; } else if (!strcmp(key, "stream")) { if (!json_bool(&p, &r->stream)) { free(key); goto bad; } } else if (!strcmp(key, "stream_options")) { if (!parse_stream_options(&p, &r->stream_include_usage)) { free(key); goto bad; } } else if (!strcmp(key, "thinking")) { if (!parse_thinking_control_value(&p, &thinking_enabled)) { free(key); goto bad; } got_thinking = true; } else if (!strcmp(key, "reasoning_effort")) { if (!parse_reasoning_effort_value(&p, &reasoning_effort)) { free(key); goto bad; } } else if (!strcmp(key, "think")) { if (!json_bool(&p, &thinking_enabled)) { free(key); goto bad; } got_thinking = true; } else if (!strcmp(key, "stop")) { if (!parse_stop(&p, &r->stops)) { free(key); goto bad; } } else if (!json_skip_value(&p)) { free(key); goto bad; } free(key); json_ws(&p); if (*p == ',') p++; json_ws(&p); } if (*p != '}') goto bad; if (!prompt) { snprintf(err, errlen, "missing prompt"); request_free(r); return false; } if (!got_thinking && model_alias_disables_thinking(r->model)) thinking_enabled = false; if (!got_thinking && model_alias_enables_thinking(r->model)) thinking_enabled = true; r->think_mode = ds4_think_mode_for_context( think_mode_from_enabled(thinking_enabled, reasoning_effort), ctx_size); chat_msgs msgs = {0}; chat_msg sys = {0}; sys.role = xstrdup("system"); sys.content = xstrdup("You are a helpful assistant"); chat_msgs_push(&msgs, sys); chat_msg user_msg = {0}; user_msg.role = xstrdup("user"); user_msg.content = prompt; prompt = NULL; chat_msgs_push(&msgs, user_msg); r->prompt_text = render_chat_prompt_text_for_syntax( r->model_syntax, &msgs, NULL, NULL, r->think_mode); ds4_tokenize_rendered_chat(e, r->prompt_text, &r->prompt); chat_msgs_free(&msgs); free(prompt); return true; bad: free(prompt); snprintf(err, errlen, "invalid JSON request"); request_free(r); return false; } static long long wall_ms(void) { struct timeval tv; gettimeofday(&tv, NULL); return (long long)tv.tv_sec * 1000 + tv.tv_usec / 1000; } static bool send_all(int fd, const void *p, size_t n) { const char *s = p; long long deadline = wall_ms() + DS4_SERVER_SEND_STALL_TIMEOUT_MS; while (n) { if (g_stop_requested) return false; ssize_t w = send(fd, s, n, 0); if (w < 0 && errno == EINTR) continue; if (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { long long remaining = deadline - wall_ms(); if (remaining <= 0) return false; struct pollfd pfd = {.fd = fd, .events = POLLOUT}; int timeout = remaining > 50 ? 50 : (int)remaining; int rc; do { rc = poll(&pfd, 1, timeout); } while (rc < 0 && errno == EINTR); if (rc < 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) return false; continue; } if (w <= 0) return false; s += w; n -= (size_t)w; deadline = wall_ms() + DS4_SERVER_SEND_STALL_TIMEOUT_MS; } return true; } static void json_escape(buf *b, const char *s) { buf_putc(b, '"'); for (; *s; s++) { unsigned char c = (unsigned char)*s; if (c == '"' || c == '\\') { buf_putc(b, '\\'); buf_putc(b, (char)c); } else if (c == '\n') { buf_puts(b, "\\n"); } else if (c == '\r') { buf_puts(b, "\\r"); } else if (c == '\t') { buf_puts(b, "\\t"); } else if (c < 0x20) { buf_printf(b, "\\u%04x", (unsigned)c); } else { buf_putc(b, (char)c); } } buf_putc(b, '"'); } static void json_escape_n(buf *b, const char *s, size_t n) { char *tmp = xstrndup(s ? s : "", n); json_escape(b, tmp); free(tmp); } static void json_escape_fragment_n(buf *b, const char *s, size_t n) { for (size_t i = 0; i < n; i++) { unsigned char c = (unsigned char)s[i]; if (c == '"' || c == '\\') { buf_putc(b, '\\'); buf_putc(b, (char)c); } else if (c == '\n') { buf_puts(b, "\\n"); } else if (c == '\r') { buf_puts(b, "\\r"); } else if (c == '\t') { buf_puts(b, "\\t"); } else if (c < 0x20) { buf_printf(b, "\\u%04x", (unsigned)c); } else { buf_putc(b, (char)c); } } } #define DS4_DSML "|DSML|" #define DS4_DSML_SHORT "DSML|" #define DS4_TOOL_CALLS_START "<" DS4_DSML "tool_calls>" #define DS4_TOOL_CALLS_END "" #define DS4_INVOKE_START "<" DS4_DSML "invoke" #define DS4_INVOKE_END "" #define DS4_PARAM_START "<" DS4_DSML "parameter" #define DS4_PARAM_END "" #define DS4_TOOL_CALLS_START_SHORT "<" DS4_DSML_SHORT "tool_calls>" #define DS4_TOOL_CALLS_END_SHORT "" #define DS4_INVOKE_START_SHORT "<" DS4_DSML_SHORT "invoke" #define DS4_INVOKE_END_SHORT "" #define DS4_PARAM_START_SHORT "<" DS4_DSML_SHORT "parameter" #define DS4_PARAM_END_SHORT "" static const char *find_any_tool_start(const char *s) { const char *best = NULL; const char *candidates[] = { strstr(s, DS41_TOOL_CALLS_START), strstr(s, DS4_TOOL_CALLS_START), strstr(s, DS4_TOOL_CALLS_START_SHORT), strstr(s, ""), strstr(s, ""), }; for (size_t i = 0; i < sizeof(candidates)/sizeof(candidates[0]); i++) { if (candidates[i] && (!best || candidates[i] < best)) best = candidates[i]; } return best; } static const char *find_any_tool_end(const char *s) { const char *best = NULL; const char *candidates[] = { strstr(s, DS41_TOOL_CALLS_END), strstr(s, DS4_TOOL_CALLS_END), strstr(s, DS4_TOOL_CALLS_END_SHORT), strstr(s, ""), strstr(s, ""), }; for (size_t i = 0; i < sizeof(candidates)/sizeof(candidates[0]); i++) { if (candidates[i] && (!best || candidates[i] < best)) best = candidates[i]; } return best; } static size_t trim_tool_separator_ws(const char *raw, size_t start, size_t limit) { while (limit > start && isspace((unsigned char)raw[limit - 1])) limit--; return limit; } static const char *skip_ascii_ws(const char *p) { while (*p && isspace((unsigned char)*p)) p++; return p; } static const char *find_last_substr(const char *s, const char *needle) { if (!s || !needle || !needle[0]) return NULL; const char *last = NULL; const char *p = s; while ((p = strstr(p, needle)) != NULL) { last = p; p++; } return last; } /* Tool arguments are literal data, including protocol-looking text. Search * for control markers only outside their value/key wrappers. */ static const char *find_tool_structural_text(const char *s, const char *needle, bool last) { static const char *wrappers[][2] = { {DS41_PARAM_START, DS41_PARAM_END}, {DS4_PARAM_START, DS4_PARAM_END}, {DS4_PARAM_START_SHORT, DS4_PARAM_END_SHORT}, {""}, {"", ""}, {"", ""}, }; const char *found = NULL; if (!s) return NULL; while (*s) { if (!strncmp(s, needle, strlen(needle))) { found = s; if (!last) return found; } bool skipped = false; for (size_t i = 0; i < sizeof(wrappers) / sizeof(wrappers[0]); i++) { if (strncmp(s, wrappers[i][0], strlen(wrappers[i][0]))) continue; const char *tag_end = strchr(s, '>'); const char *end = tag_end ? strstr(tag_end + 1, wrappers[i][1]) : NULL; if (!end) return found; s = end + strlen(wrappers[i][1]); skipped = true; break; } if (!skipped) s++; } return found; } /* Attributes are XML-escaped. Argument bodies are literal data and use the * narrower closing-delimiter escape from ds4_tool_text.h instead. */ static char *dsml_unescape_text(const char *s) { buf b = {0}; for (s = s ? s : ""; *s; s++) { if (*s != '&') { buf_putc(&b, *s); } else if (!strncmp(s, "&", 5)) { buf_putc(&b, '&'); s += 4; } else if (!strncmp(s, "<", 4)) { buf_putc(&b, '<'); s += 3; } else if (!strncmp(s, ">", 4)) { buf_putc(&b, '>'); s += 3; } else if (!strncmp(s, """, 6)) { buf_putc(&b, '"'); s += 5; } else if (!strncmp(s, "'", 6)) { buf_putc(&b, '\''); s += 5; } else { buf_putc(&b, '&'); } } return buf_take(&b); } static char *dsml_attr(const char *tag, const char *name) { char pat[64]; snprintf(pat, sizeof(pat), "%s=\"", name); const char *p = strstr(tag, pat); if (!p) return NULL; p += strlen(pat); const char *q = strchr(p, '"'); if (!q) return NULL; char *raw = xstrndup(p, (size_t)(q - p)); char *decoded = dsml_unescape_text(raw); free(raw); return decoded; } static void tool_call_json_args_add(buf *args, const char *name, const char *value, const char *is_string) { if (args->len) buf_puts(args, ", "); json_escape(args, name ? name : ""); buf_puts(args, ": "); if (is_string && !strcmp(is_string, "true")) { json_escape(args, value ? value : ""); } else { char *min = json_minify_raw_value(value ? value : "null"); buf_puts(args, min && min[0] ? min : "null"); free(min); } } /* DSML produced by the model is usually a flat list of typed parameters: * * /tmp/x * 10 * * Long generations sometimes drift into a looser XML-ish shape, omitting the * outer string attribute and putting child parameters inside it. The server * does not know client tool schemas, so it cannot make that semantically * perfect. Still, returning a structured JSON value lets the client/tool layer * reject or repair the call, which is much better than aborting the assistant * turn and losing the whole sampled continuation. */ static bool dsml_parse_leaf_param_json(const char **p_in, const char *param_start, const char *param_end, buf *out) { const char *p = *p_in; if (strncmp(p, param_start, strlen(param_start)) != 0) return false; const char *tag_end = strchr(p, '>'); if (!tag_end) return false; char *tag = xstrndup(p, (size_t)(tag_end - p + 1)); char *name = dsml_attr(tag, "name"); char *is_string = dsml_attr(tag, "string"); free(tag); if (!name) { free(is_string); return false; } const char *value_start = tag_end + 1; const char *value_end = strstr(value_start, param_end); if (!value_end) { free(name); free(is_string); return false; } char *raw_value = xstrndup(value_start, (size_t)(value_end - value_start)); const char *type = is_string ? is_string : "true"; char *value = xstrdup(raw_value); if (!strcmp(type, "true")) ds4_tool_text_unescape(value, param_end); tool_call_json_args_add(out, name, value, type); free(name); free(is_string); free(raw_value); free(value); *p_in = value_end + strlen(param_end); return true; } static bool dsml_parse_nested_params_object(const char **p_in, const char *param_start, const char *param_end, buf *out) { const char *p = *p_in; buf members = {0}; bool any = false; for (;;) { p = skip_ascii_ws(p); if (strncmp(p, param_start, strlen(param_start)) != 0) break; if (!dsml_parse_leaf_param_json(&p, param_start, param_end, &members)) { buf_free(&members); return false; } any = true; } if (!any) { buf_free(&members); return false; } buf_putc(out, '{'); buf_puts(out, members.ptr ? members.ptr : ""); buf_putc(out, '}'); buf_free(&members); *p_in = p; return true; } static void split_reasoning_content(const char *text, size_t n, char **content_out, char **reasoning_out) { char *s = xstrndup(text ? text : "", n); char *body = s; if (!strncmp(body, "", 7)) body += 7; char *think_end = strstr(body, ""); if (think_end) { *think_end = '\0'; *reasoning_out = xstrdup(body); *content_out = xstrdup(think_end + 8); } else { *reasoning_out = NULL; *content_out = xstrdup(s); } free(s); } /* Unterminated reasoning is not an answer. * * When generation hits the token cap before arrives, the old behaviour * routed the whole unterminated buffer into content, so a truncated reasoning * chain reached clients looking exactly like a finished reply. split_reasoning_ * content() cannot tell that case apart from a legitimate non-thinking answer * (neither carries a , and the buffer does not retain the opening tag), * so the discrimination has to happen here, where require_thinking_closed * already tells us thinking was expected. * * Routing the buffer to reasoning_content and emptying content makes truncation * self-describing on the wire for every consumer, not just ones that check * finish_reason. content is set to "" rather than NULL deliberately: the JSON * writer guards NULL, but not every path in this file has been audited for it, * and an empty answer is already unmistakably not an answer. */ static void ds4_local_unterminated_reasoning(const char *text, char **content_out, char **reasoning_out) { const char *body = text ? text : ""; if (!strncmp(body, "", 7)) body += 7; *reasoning_out = xstrdup(body); *content_out = xstrdup(""); } static void ds4_unterminated_reasoning_before_tool(const char *text, size_t prefix_len, char **content_out, char **reasoning_out) { const char *body = text ? text : ""; if (prefix_len > strlen(body)) prefix_len = strlen(body); if (prefix_len >= 7 && !strncmp(body, "", 7)) { body += 7; prefix_len -= 7; } *reasoning_out = xstrndup(body, prefix_len); *content_out = xstrdup(""); } static bool parse_deepseek_generated_message_ex(const char *text, bool require_thinking_closed, char **content_out, char **reasoning_out, tool_calls *calls) { text = text ? text : ""; const char *tool_search = text; bool recovered_unclosed_tool = false; /* When thinking mode is enabled the model is expected to close * before it enters the executable assistant surface. DSML inside * reasoning is just model text: it may be a mistaken attempt, a quotation, * or an explanation of the protocol. Treating it as a real tool call * duplicates it into both reasoning and structured tool_calls, and can make * clients execute something the assistant had not actually emitted as its * post-thinking action. */ if (require_thinking_closed) { const char *think_end = find_tool_structural_text(text, "", true); if (!think_end) { const char *candidate = find_any_tool_start(text); if (!candidate || !find_any_tool_end(candidate)) { fprintf(stderr, "ds4-server: thinking not closed, ignoring incomplete DSML in reasoning\n"); ds4_local_unterminated_reasoning(text, content_out, reasoning_out); return true; } tool_search = candidate; recovered_unclosed_tool = true; } else { tool_search = think_end + 8; } } const char *start = strstr(tool_search, "\n\n" DS4_TOOL_CALLS_START); int style = 0; /* 0: DSML, 1: plain XML, 2: DSML with the first vertical bar omitted. */ if (!start) start = strstr(tool_search, DS4_TOOL_CALLS_START); if (!start) { start = strstr(tool_search, "\n\n" DS4_TOOL_CALLS_START_SHORT); style = start ? 2 : style; } if (!start) { start = strstr(tool_search, DS4_TOOL_CALLS_START_SHORT); style = start ? 2 : style; } if (!start) { start = strstr(tool_search, "\n\n"); style = start ? 1 : style; } if (!start) { start = strstr(tool_search, ""); style = start ? 1 : style; } const char *v41_start = strstr(tool_search, "\n\n" DS41_TOOL_CALLS_START); if (!v41_start) v41_start = strstr(tool_search, DS41_TOOL_CALLS_START); if (v41_start && (!start || v41_start < start)) { start = v41_start; style = 3; } if (!start) { split_reasoning_content(text, strlen(text), content_out, reasoning_out); return true; } size_t content_len = trim_tool_separator_ws(text, 0, (size_t)(start - text)); const char *raw_block_start = start; const char *tool_calls_start = DS4_TOOL_CALLS_START; const char *tool_calls_end = DS4_TOOL_CALLS_END; const char *invoke_start = DS4_INVOKE_START; const char *invoke_end = DS4_INVOKE_END; const char *param_start = DS4_PARAM_START; const char *param_end = DS4_PARAM_END; if (style == 1) { tool_calls_start = ""; tool_calls_end = ""; invoke_start = "raw_tool_text); calls->raw_tool_text = xstrndup(raw_block_start, (size_t)(raw_block_end - raw_block_start)); if (recovered_unclosed_tool) { ds4_unterminated_reasoning_before_tool(text, content_len, content_out, reasoning_out); } else { split_reasoning_content(text, content_len, content_out, reasoning_out); } return true; } if (strncmp(p, invoke_start, strlen(invoke_start)) != 0) return false; const char *tag_end = strchr(p, '>'); if (!tag_end) return false; char *tag = xstrndup(p, (size_t)(tag_end - p + 1)); char *name = dsml_attr(tag, "name"); free(tag); if (!name) return false; p = tag_end + 1; buf args = {0}; while (true) { p = skip_ascii_ws(p); if (!strncmp(p, invoke_end, strlen(invoke_end))) { p += strlen(invoke_end); break; } if (strncmp(p, param_start, strlen(param_start)) != 0) { free(name); buf_free(&args); return false; } tag_end = strchr(p, '>'); if (!tag_end) { free(name); buf_free(&args); return false; } tag = xstrndup(p, (size_t)(tag_end - p + 1)); char *param_name = dsml_attr(tag, "name"); char *param_is_string = dsml_attr(tag, "string"); free(tag); if (!param_name) { free(name); free(param_name); free(param_is_string); buf_free(&args); return false; } const char *value_start = tag_end + 1; if (!param_is_string && !strncmp(skip_ascii_ws(value_start), param_start, strlen(param_start))) { buf nested = {0}; const char *nested_p = value_start; if (!dsml_parse_nested_params_object(&nested_p, param_start, param_end, &nested)) { free(name); free(param_name); buf_free(&nested); buf_free(&args); return false; } tool_call_json_args_add(&args, param_name, nested.ptr ? nested.ptr : "{}", "false"); buf_free(&nested); p = skip_ascii_ws(nested_p); if (!strncmp(p, param_end, strlen(param_end))) { p += strlen(param_end); } free(param_name); continue; } const char *value_end = strstr(value_start, param_end); if (!value_end) { free(name); free(param_name); free(param_is_string); buf_free(&args); return false; } char *raw_value = xstrndup(value_start, (size_t)(value_end - value_start)); const char *type = param_is_string ? param_is_string : "true"; char *value = xstrdup(raw_value); if (!strcmp(type, "true")) ds4_tool_text_unescape(value, param_end); tool_call_json_args_add(&args, param_name, value, type); free(param_name); free(param_is_string); free(raw_value); free(value); p = value_end + strlen(param_end); } tool_call tc = {0}; tc.name = name; buf wrapped = {0}; buf_putc(&wrapped, '{'); buf_puts(&wrapped, args.ptr ? args.ptr : ""); buf_putc(&wrapped, '}'); tc.arguments = buf_take(&wrapped); tool_calls_push(calls, tc); buf_free(&args); } } static void trim_const_span(const char **start, const char **end) { while (*start < *end && isspace((unsigned char)**start)) (*start)++; while (*end > *start && isspace((unsigned char)(*end)[-1])) (*end)--; } static bool parse_glm_generated_message_ex(const char *text, bool require_thinking_closed, char **content_out, char **reasoning_out, tool_calls *calls) { static const char tool_start[] = ""; static const char tool_end[] = ""; static const char arg_key_start[] = ""; static const char arg_key_end[] = ""; static const char arg_value_start[] = ""; static const char arg_value_end[] = ""; text = text ? text : ""; const char *tool_search = text; bool recovered_unclosed_tool = false; if (require_thinking_closed) { const char *think_end = find_tool_structural_text(text, "", true); if (!think_end) { const char *candidate = strstr(text, tool_start); if (!candidate || !strstr(candidate, tool_end)) { fprintf(stderr, "ds4-server: thinking not closed, ignoring incomplete GLM tool calls in reasoning\n"); ds4_local_unterminated_reasoning(text, content_out, reasoning_out); return true; } tool_search = candidate; recovered_unclosed_tool = true; } else { tool_search = think_end + 8; } } const char *start = strstr(tool_search, tool_start); if (!start) { split_reasoning_content(text, strlen(text), content_out, reasoning_out); return true; } const char *raw_block_start = start; if (start >= text + 2 && start[-2] == '\n' && start[-1] == '\n') { raw_block_start = start - 2; } size_t content_len = trim_tool_separator_ws(text, 0, (size_t)(raw_block_start - text)); const char *p = start; for (;;) { p = skip_ascii_ws(p); if (strncmp(p, tool_start, strlen(tool_start)) != 0) break; p += strlen(tool_start); const char *close = find_tool_structural_text(p, tool_end, false); if (!close) return false; const char *arg = strstr(p, arg_key_start); if (arg && arg > close) arg = NULL; const char *name_start = p; const char *name_end = arg ? arg : close; trim_const_span(&name_start, &name_end); if (name_end <= name_start) return false; char *name = xstrndup(name_start, (size_t)(name_end - name_start)); p = name_end; buf args = {0}; for (;;) { p = skip_ascii_ws(p); if (!strncmp(p, tool_end, strlen(tool_end))) { p += strlen(tool_end); break; } if (strncmp(p, arg_key_start, strlen(arg_key_start)) != 0) { free(name); buf_free(&args); return false; } p += strlen(arg_key_start); const char *key_end = strstr(p, arg_key_end); if (!key_end || key_end > close) { free(name); buf_free(&args); return false; } const char *key_start = p; const char *key_trim_end = key_end; trim_const_span(&key_start, &key_trim_end); char *raw_key = xstrndup(key_start, (size_t)(key_trim_end - key_start)); char *key = xstrdup(raw_key); ds4_tool_text_unescape(key, arg_key_end); free(raw_key); p = key_end + strlen(arg_key_end); p = skip_ascii_ws(p); if (strncmp(p, arg_value_start, strlen(arg_value_start)) != 0) { free(name); free(key); buf_free(&args); return false; } p += strlen(arg_value_start); const char *value_end = strstr(p, arg_value_end); if (!value_end || value_end > close) { free(name); free(key); buf_free(&args); return false; } char *raw_value = xstrndup(p, (size_t)(value_end - p)); char *value = xstrdup(raw_value); ds4_tool_text_unescape(value, arg_value_end); tool_call_json_args_add(&args, key, value, "true"); free(key); free(raw_value); free(value); p = value_end + strlen(arg_value_end); } tool_call tc = {0}; tc.name = name; buf wrapped = {0}; buf_putc(&wrapped, '{'); buf_puts(&wrapped, args.ptr ? args.ptr : ""); buf_putc(&wrapped, '}'); tc.arguments = buf_take(&wrapped); tool_calls_push(calls, tc); buf_free(&args); const char *next = skip_ascii_ws(p); if (strncmp(next, tool_start, strlen(tool_start)) != 0) { p = next; break; } p = next; } if (calls->len == 0) return false; free(calls->raw_tool_text); calls->raw_tool_text = xstrndup(raw_block_start, (size_t)(p - raw_block_start)); if (recovered_unclosed_tool) { ds4_unterminated_reasoning_before_tool(text, content_len, content_out, reasoning_out); } else { split_reasoning_content(text, content_len, content_out, reasoning_out); } return true; } /* A parameter value is JSON when it parses as one complete non-string JSON * value (object, array, number, true/false/null); anything else is text. */ static bool qwen_param_value_is_json(const char *s) { if (!s) return false; const char *p = s; json_ws(&p); if (!*p || *p == '"') return false; char *raw = NULL; if (!json_raw_value(&p, &raw)) return false; free(raw); json_ws(&p); return *p == '\0'; } static char *qwen_strip_value_newlines(const char *start, const char *end) { if (start < end && *start == '\n') start++; if (end > start && end[-1] == '\n') end--; return xstrndup(start, (size_t)(end - start)); } /* The template puts "\n\n\n" between reasoning and answer; drop the * newline the reasoning ends with and the blank lines the answer starts with. */ static void qwen_trim_split(char **content_out, char **reasoning_out) { if (reasoning_out && *reasoning_out) { size_t n = strlen(*reasoning_out); while (n > 0 && (*reasoning_out)[n - 1] == '\n') (*reasoning_out)[--n] = '\0'; } if (content_out && *content_out) { const char *c = *content_out; while (*c == '\n') c++; if (c != *content_out) { char *t = xstrdup(c); free(*content_out); *content_out = t; } } } static bool qwen_param_declared_string(const tool_schema_orders *orders, const char *name, const char *key) { const tool_schema_order *order = tool_schema_orders_find(orders, name); if (!order) return false; for (int i = 0; i < order->len; i++) { if (strcmp(order->prop[i], key)) continue; json_args schema = {0}; bool is_string = false; if (json_args_parse(order->prop_schema[i], &schema)) { for (int j = 0; j < schema.len; j++) { const json_arg *arg = &schema.v[j]; if (!strcmp(arg->key, "type") && arg->is_string && !strcmp(arg->value, "string")) is_string = true; } } json_args_free(&schema); return is_string; } return false; } static bool parse_qwen_generated_message_ex(const char *text, bool require_thinking_closed, char **content_out, char **reasoning_out, tool_calls *calls, const tool_schema_orders *orders) { static const char tool_start[] = ""; static const char tool_end[] = ""; static const char fn_start[] = "", true); if (!think_end) { const char *candidate = strstr(text, tool_start); if (!candidate || !strstr(candidate, tool_end)) { fprintf(stderr, "ds4-server: thinking not closed, ignoring incomplete Qwen tool calls in reasoning\n"); ds4_local_unterminated_reasoning(text, content_out, reasoning_out); return true; } tool_search = candidate; recovered_unclosed_tool = true; } else { tool_search = think_end + 8; } } const char *start = strstr(tool_search, tool_start); if (!start) { split_reasoning_content(text, strlen(text), content_out, reasoning_out); qwen_trim_split(content_out, reasoning_out); return true; } const char *raw_block_start = start; while (raw_block_start > text && (raw_block_start[-1] == '\n' || raw_block_start[-1] == ' ')) raw_block_start--; size_t content_len = trim_tool_separator_ws(text, 0, (size_t)(raw_block_start - text)); const char *p = start; for (;;) { p = skip_ascii_ws(p); if (strncmp(p, tool_start, strlen(tool_start)) != 0) break; p += strlen(tool_start); p = skip_ascii_ws(p); if (strncmp(p, fn_start, strlen(fn_start)) != 0) return false; p += strlen(fn_start); const char *name_end = p + strcspn(p, "<>"); if (*name_end != '>') return false; const char *name_start = p; const char *name_trim_end = name_end; trim_const_span(&name_start, &name_trim_end); if (name_trim_end <= name_start) return false; char *name = xstrndup(name_start, (size_t)(name_trim_end - name_start)); p = name_end + 1; buf args = {0}; for (;;) { const char *q = skip_ascii_ws(p); if (!strncmp(q, fn_end, strlen(fn_end))) { p = q + strlen(fn_end); break; } if (strncmp(q, param_start, strlen(param_start)) != 0) { free(name); buf_free(&args); return false; } q += strlen(param_start); const char *key_end = q + strcspn(q, "<>"); if (*key_end != '>') { free(name); buf_free(&args); return false; } const char *key_start = q; const char *key_trim_end = key_end; trim_const_span(&key_start, &key_trim_end); char *key = xstrndup(key_start, (size_t)(key_trim_end - key_start)); const char *value_start = key_end + 1; /* Only the parameter delimiter ends its value. Tool/function * markers inside argument data are literal text; check the call * boundary after consuming the parameters and . */ const char *value_end = strstr(value_start, param_end); if (!value_end) { free(name); free(key); buf_free(&args); return false; } char *value = qwen_strip_value_newlines(value_start, value_end); const bool is_string = qwen_param_declared_string(orders, name, key) || !qwen_param_value_is_json(value); if (is_string) ds4_tool_text_unescape(value, param_end); tool_call_json_args_add(&args, key, value, is_string ? "true" : "false"); free(key); free(value); p = value_end + strlen(param_end); } p = skip_ascii_ws(p); if (strncmp(p, tool_end, strlen(tool_end)) != 0) { free(name); buf_free(&args); return false; } p += strlen(tool_end); tool_call tc = {0}; tc.name = name; buf wrapped = {0}; buf_putc(&wrapped, '{'); buf_puts(&wrapped, args.ptr ? args.ptr : ""); buf_putc(&wrapped, '}'); tc.arguments = buf_take(&wrapped); tool_calls_push(calls, tc); buf_free(&args); const char *next = skip_ascii_ws(p); if (strncmp(next, tool_start, strlen(tool_start)) != 0) { p = next; break; } p = next; } if (calls->len == 0) return false; free(calls->raw_tool_text); calls->raw_tool_text = xstrndup(raw_block_start, (size_t)(p - raw_block_start)); if (recovered_unclosed_tool) { ds4_unterminated_reasoning_before_tool(text, content_len, content_out, reasoning_out); } else { split_reasoning_content(text, content_len, content_out, reasoning_out); } qwen_trim_split(content_out, reasoning_out); return true; } static bool parse_generated_message_ex_for_syntax(server_model_syntax syntax, const char *text, bool require_thinking_closed, char **content_out, char **reasoning_out, tool_calls *calls) { if (syntax == SERVER_MODEL_SYNTAX_GLM) { return parse_glm_generated_message_ex(text, require_thinking_closed, content_out, reasoning_out, calls); } if (syntax == SERVER_MODEL_SYNTAX_QWEN) { return parse_qwen_generated_message_ex(text, require_thinking_closed, content_out, reasoning_out, calls, NULL); } return parse_deepseek_generated_message_ex(text, require_thinking_closed, content_out, reasoning_out, calls); } static DS4_SERVER_MAYBE_UNUSED bool parse_generated_message_ex( const char *text, bool require_thinking_closed, char **content_out, char **reasoning_out, tool_calls *calls) { return parse_generated_message_ex_for_syntax(SERVER_MODEL_SYNTAX_DEEPSEEK, text, require_thinking_closed, content_out, reasoning_out, calls); } /* Try to repair a truncated DSML block. * * Only the outer wrapper may be missing. An unfinished invocation can still * be missing arguments; an unfinished value can be a truncated shell command * or file body. Closing either would invent an executable action. * * Returns true if repair was applied, false if the text had no recognizable DSML * or was already balanced. This deliberately does not rewrite malformed but * balanced DSML into assistant text; semantic recovery belongs to the model. */ static bool try_repair_dsml(const char *s, size_t len, buf *out) { if (!s || !len) return false; /* Only scan DSML tags after the last . DSML mentioned inside * reasoning is not executable — it inflates tag counts and causes false * positive repairs. If no is found, scan from the start * (thinking mode is not active or thinking was never opened). */ const char *think_end = find_tool_structural_text(s, "", true); const char *scan_start = think_end ? (think_end + 8) : s; size_t scan_len = (size_t)((s + len) - scan_start); /* Detect style from first tag */ const char *ts, *te, *is, *ie, *ps, *pe; const char *first = find_any_tool_start(scan_start); if (!first) return false; if (!strncmp(first, DS41_TOOL_CALLS_START, strlen(DS41_TOOL_CALLS_START))) { ts = DS41_TOOL_CALLS_START; te = DS41_TOOL_CALLS_END; is = DS41_INVOKE_START; ie = DS41_INVOKE_END; ps = DS41_PARAM_START; pe = DS41_PARAM_END; } else if (!strncmp(first, DS4_TOOL_CALLS_START, strlen(DS4_TOOL_CALLS_START))) { ts = DS4_TOOL_CALLS_START; te = DS4_TOOL_CALLS_END; is = DS4_INVOKE_START; ie = DS4_INVOKE_END; ps = DS4_PARAM_START; pe = DS4_PARAM_END; } else if (!strncmp(first, DS4_TOOL_CALLS_START_SHORT, strlen(DS4_TOOL_CALLS_START_SHORT))) { ts = DS4_TOOL_CALLS_START_SHORT; te = DS4_TOOL_CALLS_END_SHORT; is = DS4_INVOKE_START_SHORT; ie = DS4_INVOKE_END_SHORT; ps = DS4_PARAM_START_SHORT; pe = DS4_PARAM_END_SHORT; } else if (!strncmp(first, "", strlen(""))) { ts = ""; te = ""; is = " tos || ioe > ios || poe > pos) { /* Extra closing tags are not a truncation pattern. Refuse repair so the * unsigned differences below cannot wrap and append a huge suffix. */ return false; } /* All invocations are complete; supply only the enclosing wrapper. */ buf_puts(out, s); for (size_t i = 0; i < tos - toe; i++) buf_puts(out, te); return true; } static const char *tool_parse_failure_recovery_finish(const char *finish) { /* Once DSML failed to parse there is no executable tool call to report. * Preserve a true length stop, because callers can distinguish truncation * from a completed turn. Every other non-error tool-parse failure becomes * a normal assistant stop with the raw model text returned as content. */ if (finish && !strcmp(finish, "length")) return "length"; return "stop"; } static bool parse_generated_message_for_response_for_syntax(server_model_syntax syntax, const char *text, bool has_tools, bool saw_tool_start, bool require_thinking_closed, const char **finish_io, char *err, size_t errlen, char **content_out, char **reasoning_out, tool_calls *calls, bool *recovered_out, const tool_schema_orders *orders) { if (recovered_out) *recovered_out = false; bool parsed_ok = syntax == SERVER_MODEL_SYNTAX_QWEN ? parse_qwen_generated_message_ex(text, require_thinking_closed, content_out, reasoning_out, calls, orders) : parse_generated_message_ex_for_syntax(syntax, text ? text : "", require_thinking_closed, content_out, reasoning_out, calls); if (parsed_ok) return true; free(*content_out); free(*reasoning_out); *content_out = xstrdup(text ? text : ""); *reasoning_out = NULL; tool_calls_free(calls); /* A malformed tool block is model output, not a server failure. The * generation worker may hide this turn from the client, append a tool error * plus protocol reminder to the live session, and let the model try again. * If that continuation is unavailable, parsed_content keeps the raw text as * a last-resort assistant fallback instead of crashing the request. */ const char *finish = finish_io && *finish_io ? *finish_io : "stop"; if (has_tools && saw_tool_start && strcmp(finish, "error") != 0) { if (finish_io) *finish_io = tool_parse_failure_recovery_finish(finish); if (err && errlen) snprintf(err, errlen, "invalid tool call"); if (recovered_out) *recovered_out = true; } return false; } static DS4_SERVER_MAYBE_UNUSED bool parse_generated_message_for_response( const char *text, bool has_tools, bool saw_tool_start, bool require_thinking_closed, const char **finish_io, char *err, size_t errlen, char **content_out, char **reasoning_out, tool_calls *calls, bool *recovered_out) { return parse_generated_message_for_response_for_syntax( SERVER_MODEL_SYNTAX_DEEPSEEK, text, has_tools, saw_tool_start, require_thinking_closed, finish_io, err, errlen, content_out, reasoning_out, calls, recovered_out, NULL); } static void append_json_object_string(buf *b, const char *json) { buf tmp = {0}; append_json_object_or_empty(&tmp, json); json_escape(b, tmp.ptr ? tmp.ptr : "{}"); buf_free(&tmp); } static void append_tool_calls_json(buf *b, const tool_calls *calls, const char *id_prefix, const tool_schema_orders *orders) { (void)orders; buf_putc(b, '['); for (int i = 0; i < calls->len; i++) { const tool_call *tc = &calls->v[i]; if (i) buf_putc(b, ','); char idbuf[128]; snprintf(idbuf, sizeof(idbuf), "%s_tool_%d", id_prefix, i); buf_puts(b, "{\"id\":"); json_escape(b, tc->id ? tc->id : idbuf); buf_puts(b, ",\"type\":\"function\",\"function\":{\"name\":"); json_escape(b, tc->name ? tc->name : ""); buf_puts(b, ",\"arguments\":"); append_json_object_string(b, tc->arguments); buf_puts(b, "}}"); } buf_putc(b, ']'); } static void append_tool_call_deltas_json(buf *b, const tool_calls *calls, const char *id_prefix, const tool_schema_orders *orders) { (void)orders; buf_putc(b, '['); for (int i = 0; i < calls->len; i++) { const tool_call *tc = &calls->v[i]; if (i) buf_putc(b, ','); char idbuf[128]; snprintf(idbuf, sizeof(idbuf), "%s_tool_%d", id_prefix, i); buf_puts(b, "{\"index\":"); buf_printf(b, "%d", i); buf_puts(b, ",\"id\":"); json_escape(b, tc->id ? tc->id : idbuf); buf_puts(b, ",\"type\":\"function\",\"function\":{\"name\":"); json_escape(b, tc->name ? tc->name : ""); buf_puts(b, ",\"arguments\":"); append_json_object_string(b, tc->arguments); buf_puts(b, "}}"); } buf_putc(b, ']'); } static void append_cors_headers(buf *h) { buf_puts(h, "Access-Control-Allow-Origin: *\r\n" "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" "Access-Control-Allow-Headers: *\r\n"); } static bool http_response(int fd, bool enable_cors, int code, const char *type, const char *body) { const char *reason = code == 200 ? "OK" : code == 204 ? "No Content" : code == 400 ? "Bad Request" : code == 404 ? "Not Found" : code == 409 ? "Conflict" : code == 500 ? "Internal Server Error" : "Error"; const size_t body_len = body ? strlen(body) : 0; buf h = {0}; buf_printf(&h, "HTTP/1.1 %d %s\r\n" "Content-Length: %zu\r\n", code, reason, body_len); if (type && type[0]) { buf_puts(&h, "Content-Type: "); buf_puts(&h, type); buf_puts(&h, "\r\n"); } if (enable_cors) append_cors_headers(&h); buf_puts(&h, "Connection: close\r\n\r\n"); bool ok = send_all(fd, h.ptr, h.len); if (ok && body_len) ok = send_all(fd, body, body_len); buf_free(&h); return ok; } static bool http_error(int fd, bool enable_cors, int code, const char *msg) { buf b = {0}; buf_puts(&b, "{\"error\":{\"message\":"); json_escape(&b, msg); buf_puts(&b, ",\"type\":\"invalid_request_error\"}}\n"); bool ok = http_response(fd, enable_cors, code, "application/json", b.ptr); buf_free(&b); return ok; } static const char *context_length_error_param(const request *r) { if (!r) return "prompt"; if (r->api == API_RESPONSES) return "input"; return r->kind == REQ_COMPLETION ? "prompt" : "messages"; } static bool request_exceeds_context(const request *r, int ctx_size) { /* ds4_session_sync() rejects prompt->len >= ctx_size because generation * needs at least one free context slot. Catch the same boundary here so * clients get a normal protocol error instead of a later backend failure. */ return r && r->prompt.len >= ctx_size; } static bool http_error_context_length_exceeded(int fd, bool enable_cors, const request *r, int n_prompt_tokens, int ctx_size) { buf b = {0}; char msg[160]; snprintf(msg, sizeof(msg), "Prompt has %d tokens, but the configured context size is %d tokens", n_prompt_tokens, ctx_size); if (r && r->api == API_ANTHROPIC) { buf_puts(&b, "{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":"); json_escape(&b, msg); buf_puts(&b, ",\"n_prompt_tokens\":"); buf_printf(&b, "%d", n_prompt_tokens); buf_puts(&b, ",\"n_ctx\":"); buf_printf(&b, "%d", ctx_size); buf_puts(&b, "}}\n"); } else { buf_puts(&b, "{\"error\":{\"message\":"); json_escape(&b, msg); buf_puts(&b, ",\"type\":\"invalid_request_error\",\"param\":"); json_escape(&b, context_length_error_param(r)); buf_puts(&b, ",\"code\":\"context_length_exceeded\",\"n_prompt_tokens\":"); buf_printf(&b, "%d", n_prompt_tokens); buf_puts(&b, ",\"n_ctx\":"); buf_printf(&b, "%d", ctx_size); buf_puts(&b, "}}\n"); } bool ok = http_response(fd, enable_cors, 400, "application/json", b.ptr); buf_free(&b); return ok; } /* Streaming is a translation state machine over the raw DS4 text. The model * may produce and DSML tool blocks; clients should receive those as * protocol-native reasoning/tool deltas, never as visible assistant text. */ static bool sse_headers(int fd, bool enable_cors) { buf h = {0}; buf_puts(&h, "HTTP/1.1 200 OK\r\n" "Content-Type: text/event-stream\r\n" "Cache-Control: no-cache\r\n"); if (enable_cors) append_cors_headers(&h); buf_puts(&h, "Connection: close\r\n\r\n"); bool ok = send_all(fd, h.ptr, h.len); buf_free(&h); return ok; } static bool sse_error_event(int fd, const request *r, const char *msg) { const char *message = msg && msg[0] ? msg : "internal server error"; buf b = {0}; if (r && r->api == API_ANTHROPIC) { buf_puts(&b, "event: error\ndata: {\"type\":\"error\",\"error\":{\"type\":\"api_error\",\"message\":"); json_escape(&b, message); buf_puts(&b, "}}\n\n"); } else { buf_puts(&b, "event: error\ndata: {\"error\":{\"message\":"); json_escape(&b, message); buf_puts(&b, ",\"type\":\"server_error\"}}\n\n"); } bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } static bool sse_chunk(int fd, const request *r, const char *id, const char *text, const char *finish) { buf b = {0}; long now = (long)time(NULL); if (r->kind == REQ_CHAT) { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":"); if (text) { buf_puts(&b, "{\"content\":"); json_escape(&b, text); buf_putc(&b, '}'); } else { buf_puts(&b, finish ? "{}" : "{\"role\":\"assistant\"}"); } buf_puts(&b, ",\"finish_reason\":"); if (finish) json_escape(&b, finish); else buf_puts(&b, "null"); buf_puts(&b, "}]}\n\n"); } else { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"text_completion\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"text\":"); json_escape(&b, text ? text : ""); buf_puts(&b, ",\"index\":0,\"finish_reason\":"); if (finish) json_escape(&b, finish); else buf_puts(&b, "null"); buf_puts(&b, "}]}\n\n"); } bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } static int clamp_usage_tokens(int value, int max) { if (value < 0) return 0; if (max >= 0 && value > max) return max; return value; } static void append_openai_usage_json(buf *b, const request *r, int prompt_tokens, int completion_tokens) { int cached_tokens = r ? r->cache_read_tokens : 0; int cache_write_tokens = r ? r->cache_write_tokens : 0; cached_tokens = clamp_usage_tokens(cached_tokens, prompt_tokens); cache_write_tokens = clamp_usage_tokens(cache_write_tokens, prompt_tokens - cached_tokens); /* OpenAI defines cached_tokens as prompt tokens retrieved from cache. * Newly-prefilled tokens are useful to expose, but they are a DS4 extension * and must stay separate so OpenAI-compatible clients do not over-count * cache hits. */ buf_printf(b, "{\"prompt_tokens\":%d,\"completion_tokens\":%d,\"total_tokens\":%d," "\"prompt_tokens_details\":{\"cached_tokens\":%d,\"cache_write_tokens\":%d}}", prompt_tokens, completion_tokens, prompt_tokens + completion_tokens, cached_tokens, cache_write_tokens); } static bool sse_usage_chunk(int fd, const request *r, const char *id, int prompt_tokens, int completion_tokens) { if (!r->stream_include_usage) return true; buf b = {0}; long now = (long)time(NULL); if (r->kind == REQ_CHAT) { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[],\"usage\":"); } else { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"text_completion\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[],\"usage\":"); } append_openai_usage_json(&b, r, prompt_tokens, completion_tokens); buf_puts(&b, "}\n\n"); bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } static bool sse_done(int fd, const request *r, const char *id, int prompt_tokens, int completion_tokens) { return sse_usage_chunk(fd, r, id, prompt_tokens, completion_tokens) && send_all(fd, "data: [DONE]\n\n", 14); } static bool sse_chat_finish(int fd, const request *r, const char *id, const char *content, const char *reasoning, const tool_calls *calls, const char *finish, int prompt_tokens, int completion_tokens) { if (!sse_chunk(fd, r, id, NULL, NULL)) return false; buf b = {0}; long now = (long)time(NULL); if (reasoning && reasoning[0]) { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":"); json_escape(&b, reasoning); buf_puts(&b, "},\"finish_reason\":null}]}\n\n"); } if (content && content[0]) { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{\"content\":"); json_escape(&b, content); buf_puts(&b, "},\"finish_reason\":null}]}\n\n"); } if (calls && calls->len) { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":"); append_tool_call_deltas_json(&b, calls, id, &r->tool_orders); buf_puts(&b, "},\"finish_reason\":null}]}\n\n"); } buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":"); json_escape(&b, finish); buf_puts(&b, "}]}\n\n"); bool ok = send_all(fd, b.ptr, b.len) && sse_done(fd, r, id, prompt_tokens, completion_tokens); buf_free(&b); return ok; } typedef enum { OPENAI_STREAM_THINKING, OPENAI_STREAM_TEXT, OPENAI_STREAM_TOOL, OPENAI_STREAM_SUPPRESS, } openai_stream_mode; typedef enum { DSML_TOOL_BETWEEN_INVOKES, DSML_TOOL_BETWEEN_PARAMS, DSML_TOOL_PARAM_VALUE, DSML_TOOL_DONE, DSML_TOOL_ERROR, } dsml_tool_stream_state; /* Shared states for protocol-specific DSML stream projections. The model * still samples DSML; these states only translate already-sampled bytes into * OpenAI / Anthropic wire events while final parsing remains authoritative. */ typedef struct { dsml_tool_stream_state state; const char *tool_calls_end; const char *invoke_start; const char *invoke_end; const char *param_start; const char *param_end; size_t parse_pos; int index; bool active; bool emitted_any; bool args_open; bool first_param; bool param_is_string; char **ids; int ids_cap; } openai_tool_stream; typedef struct { openai_stream_mode mode; size_t emit_pos; bool active; bool checked_think_prefix; bool guard_second_reasoning; bool sent_reasoning; bool sent_content; openai_tool_stream tool; } openai_stream; static bool stream_needs_second_reasoning_guard(const request *r) { return ds4_think_mode_enabled(r->think_mode) && r->has_tools && r->model_syntax != SERVER_MODEL_SYNTAX_QWEN; } static void openai_stream_start(const request *r, openai_stream *st) { memset(st, 0, sizeof(*st)); st->active = true; st->mode = ds4_think_mode_enabled(r->think_mode) ? OPENAI_STREAM_THINKING : OPENAI_STREAM_TEXT; st->guard_second_reasoning = stream_needs_second_reasoning_guard(r); } static void openai_tool_stream_free(openai_tool_stream *ts) { if (!ts) return; for (int i = 0; i < ts->ids_cap; i++) free(ts->ids[i]); free(ts->ids); ts->ids = NULL; ts->ids_cap = 0; } static void openai_stream_free(openai_stream *st) { if (!st) return; openai_tool_stream_free(&st->tool); } static bool openai_tool_stream_has_id(const openai_tool_stream *ts, const char *id, int upto) { if (!ts || !id || !id[0]) return false; if (upto > ts->ids_cap) upto = ts->ids_cap; for (int i = 0; i < upto; i++) { if (ts->ids[i] && !strcmp(ts->ids[i], id)) return true; } return false; } static const char *openai_tool_stream_id(server *s, openai_tool_stream *ts, int index) { if (!ts || index < 0) return ""; if (index >= ts->ids_cap) { int old = ts->ids_cap; int cap = old ? old : 4; while (cap <= index) cap *= 2; ts->ids = xrealloc(ts->ids, (size_t)cap * sizeof(ts->ids[0])); memset(ts->ids + old, 0, (size_t)(cap - old) * sizeof(ts->ids[0])); ts->ids_cap = cap; } if (!ts->ids[index]) { char id[64]; for (;;) { random_tool_id(id, sizeof(id), API_OPENAI); if (!openai_tool_stream_has_id(ts, id, index) && !tool_memory_has_id(s, id)) break; } ts->ids[index] = xstrdup(id); } return ts->ids[index]; } static size_t text_stream_safe_limit(const char *raw, size_t start, size_t raw_len, bool has_tools, bool final); static bool sse_chat_delta_n(int fd, const request *r, const char *id, const char *field, const char *text, size_t len) { if (len == 0) return true; buf b = {0}; long now = (long)time(NULL); buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{"); json_escape(&b, field); buf_putc(&b, ':'); json_escape_n(&b, text, len); buf_puts(&b, "},\"finish_reason\":null}]}\n\n"); bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } /* OpenAI clients can consume function.arguments as a stream of JSON text * fragments. DS4 generates XML-ish DSML instead, so this parser switches to a * hidden tool mode at <...tool_calls>, emits the tool header once the invoke tag * is complete, then translates each parameter body into argument deltas while * holding only tiny tails for partial closing tags, UTF-8, and DSML entities. */ static bool sse_chat_tool_call_start_delta(int fd, const request *r, const char *id, int index, const char *tool_id, const char *name) { buf b = {0}; long now = (long)time(NULL); buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":"); buf_printf(&b, "%d", index); buf_puts(&b, ",\"id\":"); json_escape(&b, tool_id ? tool_id : ""); buf_puts(&b, ",\"type\":\"function\",\"function\":{\"name\":"); json_escape(&b, name ? name : ""); buf_puts(&b, ",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\n"); bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } static bool sse_chat_tool_call_args_delta_n(int fd, const request *r, const char *id, int index, const char *text, size_t len) { if (len == 0) return true; buf b = {0}; long now = (long)time(NULL); buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":"); buf_printf(&b, "%d", index); buf_puts(&b, ",\"function\":{\"arguments\":"); json_escape_n(&b, text, len); buf_puts(&b, "}}]},\"finish_reason\":null}]}\n\n"); bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } static bool raw_full_lit(const char *raw, size_t raw_len, size_t pos, const char *lit) { size_t n = strlen(lit); return pos <= raw_len && raw_len - pos >= n && !memcmp(raw + pos, lit, n); } static bool raw_partial_lit(const char *raw, size_t raw_len, size_t pos, const char *lit) { size_t n = strlen(lit); if (pos > raw_len || raw_len - pos >= n) return false; return !memcmp(raw + pos, lit, raw_len - pos); } static bool raw_partial_any(const char *raw, size_t raw_len, size_t pos, const char *a, const char *b) { return raw_partial_lit(raw, raw_len, pos, a) || raw_partial_lit(raw, raw_len, pos, b); } static const char *find_lit_bounded(const char *s, size_t n, const char *lit) { size_t m = strlen(lit); if (m == 0) return s; if (n < m) return NULL; for (size_t i = 0; i <= n - m; i++) { if (!memcmp(s + i, lit, m)) return s + i; } return NULL; } typedef enum { DSML_DECODE_OUTSIDE, DSML_DECODE_STRUCTURAL, DSML_DECODE_STRING_BODY, DSML_DECODE_JSON_STRUCTURAL, DSML_DECODE_JSON_STRING, } dsml_decode_state; typedef enum { DSML_TRACK_SEARCH, DSML_TRACK_STRUCTURAL, DSML_TRACK_STRING_BODY, DSML_TRACK_JSON_PARAM, DSML_TRACK_DONE, } dsml_track_mode; typedef struct { const char *tool_calls_start; const char *tool_calls_end; const char *invoke_start; const char *invoke_end; const char *param_start; const char *param_end; } dsml_syntax; static const dsml_syntax dsml_syntaxes[] = { { DS41_TOOL_CALLS_START, DS41_TOOL_CALLS_END, DS41_INVOKE_START, DS41_INVOKE_END, DS41_PARAM_START, DS41_PARAM_END, }, { DS4_TOOL_CALLS_START, DS4_TOOL_CALLS_END, DS4_INVOKE_START, DS4_INVOKE_END, DS4_PARAM_START, DS4_PARAM_END, }, { DS4_TOOL_CALLS_START_SHORT, DS4_TOOL_CALLS_END_SHORT, DS4_INVOKE_START_SHORT, DS4_INVOKE_END_SHORT, DS4_PARAM_START_SHORT, DS4_PARAM_END_SHORT, }, { "", "", "", "", }, }; static const dsml_syntax glm_tool_syntax = { "", "", "", "", "", "", }; static const dsml_syntax qwen_tool_syntax = { "", "", "", "", }; typedef struct { dsml_track_mode mode; dsml_decode_state decode; const dsml_syntax *syn; size_t pos; bool json_in_string; bool json_escaped; server_model_syntax model_syntax; } dsml_decode_tracker; static size_t dsml_max_tool_start_len(void) { size_t max = 0; for (size_t i = 0; i < sizeof(dsml_syntaxes) / sizeof(dsml_syntaxes[0]); i++) { size_t n = strlen(dsml_syntaxes[i].tool_calls_start); if (n > max) max = n; } return max; } static bool dsml_find_tool_start(const char *raw, size_t raw_len, size_t *pos_out, const dsml_syntax **syn_out) { const char *best = NULL; const dsml_syntax *best_syn = NULL; for (size_t i = 0; i < sizeof(dsml_syntaxes) / sizeof(dsml_syntaxes[0]); i++) { const char *p = find_lit_bounded(raw, raw_len, dsml_syntaxes[i].tool_calls_start); if (p && (!best || p < best)) { best = p; best_syn = &dsml_syntaxes[i]; } } if (!best) return false; *pos_out = (size_t)(best - raw) + strlen(best_syn->tool_calls_start); *syn_out = best_syn; return true; } static bool dsml_find_tool_start_from(const char *raw, size_t raw_len, size_t start, size_t *pos_out, const dsml_syntax **syn_out) { if (start > raw_len) return false; size_t rel = 0; if (!dsml_find_tool_start(raw + start, raw_len - start, &rel, syn_out)) { return false; } *pos_out = start + rel; return true; } static bool dsml_attr_is_string_true(const char *raw, size_t raw_len, size_t tag_start, size_t tag_end) { if (tag_end <= tag_start || tag_end > raw_len) return false; char *tag = xstrndup(raw + tag_start, tag_end - tag_start); char *is_string = dsml_attr(tag, "string"); bool result = is_string && !strcmp(is_string, "true"); free(is_string); free(tag); return result; } #ifdef DS4_SERVER_TEST static bool raw_suffix_partial_lit(const char *raw, size_t raw_len, const char *lit, size_t min_len) { size_t lit_len = strlen(lit); if (!raw || raw_len == 0 || lit_len == 0) return false; size_t max = raw_len < lit_len ? raw_len : lit_len - 1; for (size_t n = min_len; n <= max; n++) { if (!memcmp(raw + raw_len - n, lit, n)) return true; } return false; } static dsml_decode_state dsml_decode_scan_json_param(const char *raw, size_t raw_len, size_t pos, const dsml_syntax *syn) { bool in_string = false; bool escaped = false; while (pos < raw_len) { if (!in_string && raw_full_lit(raw, raw_len, pos, syn->param_end)) { return DSML_DECODE_STRUCTURAL; } unsigned char c = (unsigned char)raw[pos++]; if (in_string) { if (escaped) { escaped = false; } else if (c == '\\') { escaped = true; } else if (c == '"') { in_string = false; } } else if (c == '"') { in_string = true; } } if (!in_string && raw_suffix_partial_lit(raw, raw_len, syn->param_end, 2)) { return DSML_DECODE_STRUCTURAL; } return in_string ? DSML_DECODE_JSON_STRING : DSML_DECODE_JSON_STRUCTURAL; } /* Slow reference recognizer used by tests. */ static dsml_decode_state dsml_decode_state_for_text(const char *raw, size_t raw_len) { if (!raw || raw_len == 0) return DSML_DECODE_OUTSIDE; size_t pos = 0; const dsml_syntax *syn = NULL; if (!dsml_find_tool_start(raw, raw_len, &pos, &syn)) { return DSML_DECODE_OUTSIDE; } for (;;) { while (pos < raw_len && isspace((unsigned char)raw[pos])) pos++; if (pos >= raw_len) return DSML_DECODE_STRUCTURAL; if (raw_full_lit(raw, raw_len, pos, syn->tool_calls_end)) { return DSML_DECODE_OUTSIDE; } if (raw_full_lit(raw, raw_len, pos, syn->invoke_end)) { pos += strlen(syn->invoke_end); continue; } if (raw_full_lit(raw, raw_len, pos, syn->invoke_start)) { const char *tag_end = memchr(raw + pos, '>', raw_len - pos); if (!tag_end) return DSML_DECODE_STRUCTURAL; pos = (size_t)(tag_end - raw) + 1; continue; } if (raw_full_lit(raw, raw_len, pos, syn->param_start)) { size_t tag_start = pos; const char *tag_end_ptr = memchr(raw + pos, '>', raw_len - pos); if (!tag_end_ptr) return DSML_DECODE_STRUCTURAL; size_t tag_end = (size_t)(tag_end_ptr - raw) + 1; bool string_value = dsml_attr_is_string_true(raw, raw_len, tag_start, tag_end); pos = tag_end; if (string_value) { const char *end = find_lit_bounded(raw + pos, raw_len - pos, syn->param_end); if (!end) { if (raw_suffix_partial_lit(raw, raw_len, syn->param_end, 2)) { return DSML_DECODE_STRUCTURAL; } return DSML_DECODE_STRING_BODY; } pos = (size_t)(end - raw) + strlen(syn->param_end); continue; } dsml_decode_state json_state = dsml_decode_scan_json_param(raw, raw_len, pos, syn); if (json_state == DSML_DECODE_STRUCTURAL) { const char *end = find_lit_bounded(raw + pos, raw_len - pos, syn->param_end); if (!end) return DSML_DECODE_STRUCTURAL; pos = (size_t)(end - raw) + strlen(syn->param_end); continue; } return json_state; } for (size_t i = 0; i < sizeof(dsml_syntaxes) / sizeof(dsml_syntaxes[0]); i++) { if (raw_partial_lit(raw, raw_len, pos, dsml_syntaxes[i].tool_calls_end) || raw_partial_lit(raw, raw_len, pos, dsml_syntaxes[i].invoke_start) || raw_partial_lit(raw, raw_len, pos, dsml_syntaxes[i].invoke_end) || raw_partial_lit(raw, raw_len, pos, dsml_syntaxes[i].param_start) || raw_partial_lit(raw, raw_len, pos, dsml_syntaxes[i].param_end)) { return DSML_DECODE_STRUCTURAL; } } return DSML_DECODE_STRUCTURAL; } } #endif static bool dsml_decode_state_is_tool(dsml_decode_state state) { return state != DSML_DECODE_OUTSIDE; } static bool dsml_decode_state_uses_payload_sampling(dsml_decode_state state) { return state == DSML_DECODE_STRING_BODY || state == DSML_DECODE_JSON_STRING; } static void dsml_decode_tracker_init(dsml_decode_tracker *dt) { memset(dt, 0, sizeof(*dt)); dt->mode = DSML_TRACK_SEARCH; dt->decode = DSML_DECODE_OUTSIDE; } /* Track where generation is inside a DSML tool call. This is intentionally a * forgiving recognizer, not a validator: malformed DSML still gets parsed later * by the normal tool-call parser. Here we only need enough state to decide * whether the next token belongs to protocol syntax or arbitrary payload. */ static void dsml_decode_tracker_update(dsml_decode_tracker *dt, const char *raw, size_t raw_len) { if (!dt || !raw) return; for (;;) { if (dt->mode == DSML_TRACK_DONE) { dt->decode = DSML_DECODE_OUTSIDE; return; } if (dt->mode == DSML_TRACK_SEARCH) { size_t pos = 0; const dsml_syntax *syn = NULL; bool found; if (dt->model_syntax == SERVER_MODEL_SYNTAX_GLM || dt->model_syntax == SERVER_MODEL_SYNTAX_QWEN) { syn = dt->model_syntax == SERVER_MODEL_SYNTAX_QWEN ? &qwen_tool_syntax : &glm_tool_syntax; const char *start = find_lit_bounded(raw + dt->pos, raw_len - dt->pos, syn->tool_calls_start); found = start != NULL; if (found) { pos = (size_t)(start - raw) + strlen(syn->tool_calls_start); } } else { found = dsml_find_tool_start_from(raw, raw_len, dt->pos, &pos, &syn); } if (!found) { size_t hold = dsml_max_tool_start_len(); dt->pos = raw_len > hold ? raw_len - hold : 0; dt->decode = DSML_DECODE_OUTSIDE; return; } dt->syn = syn; dt->pos = pos; dt->mode = DSML_TRACK_STRUCTURAL; dt->decode = DSML_DECODE_STRUCTURAL; } if (dt->mode == DSML_TRACK_STRING_BODY) { while (dt->pos < raw_len) { if (raw_full_lit(raw, raw_len, dt->pos, dt->syn->param_end)) { dt->pos += strlen(dt->syn->param_end); dt->mode = DSML_TRACK_STRUCTURAL; dt->decode = DSML_DECODE_STRUCTURAL; goto structural; } if (raw_partial_lit(raw, raw_len, dt->pos, dt->syn->param_end)) { /* Keep even a lone '<' for the next update, without * making ordinary code/HTML switch to greedy sampling. */ dt->decode = raw_len - dt->pos >= 2 ? DSML_DECODE_STRUCTURAL : DSML_DECODE_STRING_BODY; return; } dt->pos++; } dt->decode = DSML_DECODE_STRING_BODY; return; } if (dt->mode == DSML_TRACK_JSON_PARAM) { while (dt->pos < raw_len) { if (!dt->json_in_string) { if (raw_full_lit(raw, raw_len, dt->pos, dt->syn->param_end)) { dt->pos += strlen(dt->syn->param_end); dt->mode = DSML_TRACK_STRUCTURAL; dt->decode = DSML_DECODE_STRUCTURAL; goto structural; } if (raw_partial_lit(raw, raw_len, dt->pos, dt->syn->param_end)) { dt->decode = raw_len - dt->pos >= 2 ? DSML_DECODE_STRUCTURAL : DSML_DECODE_JSON_STRUCTURAL; return; } } unsigned char c = (unsigned char)raw[dt->pos++]; if (dt->json_in_string) { if (dt->json_escaped) { dt->json_escaped = false; } else if (c == '\\') { dt->json_escaped = true; } else if (c == '"') { dt->json_in_string = false; } } else if (c == '"') { dt->json_in_string = true; } } dt->decode = dt->json_in_string ? DSML_DECODE_JSON_STRING : DSML_DECODE_JSON_STRUCTURAL; return; } structural: while (dt->mode == DSML_TRACK_STRUCTURAL) { while (dt->pos < raw_len && isspace((unsigned char)raw[dt->pos])) dt->pos++; if (dt->pos >= raw_len) { dt->decode = DSML_DECODE_STRUCTURAL; return; } if (raw_full_lit(raw, raw_len, dt->pos, dt->syn->tool_calls_end)) { dt->mode = DSML_TRACK_DONE; dt->pos += strlen(dt->syn->tool_calls_end); dt->decode = DSML_DECODE_OUTSIDE; return; } if (raw_full_lit(raw, raw_len, dt->pos, dt->syn->invoke_end)) { dt->pos += strlen(dt->syn->invoke_end); continue; } if (raw_full_lit(raw, raw_len, dt->pos, dt->syn->invoke_start)) { const char *tag_end = memchr(raw + dt->pos, '>', raw_len - dt->pos); if (!tag_end) { dt->decode = DSML_DECODE_STRUCTURAL; return; } dt->pos = (size_t)(tag_end - raw) + 1; continue; } if (raw_full_lit(raw, raw_len, dt->pos, dt->syn->param_start)) { size_t tag_start = dt->pos; const char *tag_end = memchr(raw + dt->pos, '>', raw_len - dt->pos); if (!tag_end) { dt->decode = DSML_DECODE_STRUCTURAL; return; } size_t tag_after = (size_t)(tag_end - raw) + 1; bool string_value = dt->model_syntax == SERVER_MODEL_SYNTAX_GLM || dt->model_syntax == SERVER_MODEL_SYNTAX_QWEN || dsml_attr_is_string_true(raw, raw_len, tag_start, tag_after); dt->pos = tag_after; if (string_value) { dt->mode = DSML_TRACK_STRING_BODY; dt->decode = DSML_DECODE_STRING_BODY; } else { dt->mode = DSML_TRACK_JSON_PARAM; dt->json_in_string = false; dt->json_escaped = false; dt->decode = DSML_DECODE_JSON_STRUCTURAL; } break; } if (raw_partial_lit(raw, raw_len, dt->pos, dt->syn->tool_calls_end) || raw_partial_lit(raw, raw_len, dt->pos, dt->syn->invoke_start) || raw_partial_lit(raw, raw_len, dt->pos, dt->syn->invoke_end) || raw_partial_lit(raw, raw_len, dt->pos, dt->syn->param_start) || raw_partial_lit(raw, raw_len, dt->pos, dt->syn->param_end)) { dt->decode = DSML_DECODE_STRUCTURAL; return; } if (dt->model_syntax == SERVER_MODEL_SYNTAX_GLM) { /* GLM puts function and argument names between tags. */ dt->pos++; continue; } dt->decode = DSML_DECODE_STRUCTURAL; return; } } } static void observe_tool_markers(const dsml_decode_tracker *tracker, const char *scan, bool *saw_start, bool *saw_end, bool *orphan_end) { if (tracker->syn) *saw_start = true; if (tracker->mode == DSML_TRACK_DONE) *saw_end = true; else if (!*saw_start && scan && find_any_tool_end(scan) && orphan_end) *orphan_end = true; } static size_t tool_param_value_stream_safe_len(const char *raw, size_t start, size_t raw_len, const char *param_end, bool is_string) { size_t limit = raw_len; size_t end_len = strlen(param_end); size_t scan = raw_len > start + end_len ? raw_len - end_len : start; for (size_t i = raw_len; i > scan; i--) { if (raw[i - 1] != '<') continue; size_t marker = i - 1; size_t tail = raw_len - marker; if (tail < end_len && !memcmp(raw + marker, param_end, tail)) limit = marker; break; } if (is_string) { for (size_t i = limit; i > start; i--) { if (raw[i - 1] != '&') continue; if (ds4_tool_text_partial_escape(raw + i - 1, limit - i + 1, param_end)) limit = i - 1; break; } } return utf8_stream_safe_len(raw, start, limit, false); } static bool openai_tool_emit_args_fragment(int fd, const request *r, const char *id, openai_tool_stream *ts, const char *text, size_t len) { return sse_chat_tool_call_args_delta_n(fd, r, id, ts->index, text, len); } static bool openai_tool_emit_string_value(int fd, const request *r, const char *id, openai_tool_stream *ts, const char *text, size_t len) { if (len == 0) return true; char *raw = xstrndup(text, len); ds4_tool_text_unescape(raw, ts->param_end); buf frag = {0}; json_escape_fragment_n(&frag, raw, strlen(raw)); bool ok = openai_tool_emit_args_fragment(fd, r, id, ts, frag.ptr ? frag.ptr : "", frag.len); buf_free(&frag); free(raw); return ok; } static bool openai_tool_emit_param_prefix(int fd, const request *r, const char *id, openai_tool_stream *ts, const char *name, bool is_string) { buf frag = {0}; if (ts->first_param) ts->first_param = false; else buf_putc(&frag, ','); json_escape(&frag, name ? name : ""); buf_putc(&frag, ':'); if (is_string) buf_putc(&frag, '"'); bool ok = openai_tool_emit_args_fragment(fd, r, id, ts, frag.ptr ? frag.ptr : "", frag.len); buf_free(&frag); return ok; } static bool openai_tool_stream_init(openai_tool_stream *ts, const char *raw, size_t raw_len, size_t pos) { openai_tool_stream_free(ts); memset(ts, 0, sizeof(*ts)); ts->active = true; ts->state = DSML_TOOL_BETWEEN_INVOKES; ts->parse_pos = pos; if (raw_full_lit(raw, raw_len, pos, DS41_TOOL_CALLS_START)) { ts->parse_pos += strlen(DS41_TOOL_CALLS_START); ts->tool_calls_end = DS41_TOOL_CALLS_END; ts->invoke_start = DS41_INVOKE_START; ts->invoke_end = DS41_INVOKE_END; ts->param_start = DS41_PARAM_START; ts->param_end = DS41_PARAM_END; } else if (raw_full_lit(raw, raw_len, pos, DS4_TOOL_CALLS_START)) { ts->parse_pos += strlen(DS4_TOOL_CALLS_START); ts->tool_calls_end = DS4_TOOL_CALLS_END; ts->invoke_start = DS4_INVOKE_START; ts->invoke_end = DS4_INVOKE_END; ts->param_start = DS4_PARAM_START; ts->param_end = DS4_PARAM_END; } else if (raw_full_lit(raw, raw_len, pos, DS4_TOOL_CALLS_START_SHORT)) { ts->parse_pos += strlen(DS4_TOOL_CALLS_START_SHORT); ts->tool_calls_end = DS4_TOOL_CALLS_END_SHORT; ts->invoke_start = DS4_INVOKE_START_SHORT; ts->invoke_end = DS4_INVOKE_END_SHORT; ts->param_start = DS4_PARAM_START_SHORT; ts->param_end = DS4_PARAM_END_SHORT; } else if (raw_full_lit(raw, raw_len, pos, "")) { ts->parse_pos += strlen(""); ts->tool_calls_end = ""; ts->invoke_start = "invoke_end = ""; ts->param_start = "param_end = ""; } else { ts->active = false; ts->state = DSML_TOOL_ERROR; return false; } return true; } static bool openai_tool_stream_fail(openai_tool_stream *ts) { ts->active = false; ts->state = DSML_TOOL_ERROR; return true; } static bool openai_tool_start_invoke(int fd, server *s, const request *r, const char *id, openai_tool_stream *ts, const char *raw, size_t raw_len) { const char *tag_end = memchr(raw + ts->parse_pos, '>', raw_len - ts->parse_pos); if (!tag_end) return true; char *tag = xstrndup(raw + ts->parse_pos, (size_t)(tag_end - (raw + ts->parse_pos) + 1)); char *name = dsml_attr(tag, "name"); free(tag); if (!name) return openai_tool_stream_fail(ts); const char *tool_id = openai_tool_stream_id(s, ts, ts->index); bool ok = sse_chat_tool_call_start_delta(fd, r, id, ts->index, tool_id, name) && openai_tool_emit_args_fragment(fd, r, id, ts, "{", 1); free(name); if (!ok) return false; ts->emitted_any = true; ts->args_open = true; ts->first_param = true; ts->parse_pos = (size_t)(tag_end - raw) + 1; ts->state = DSML_TOOL_BETWEEN_PARAMS; return true; } static bool openai_tool_start_param(int fd, const request *r, const char *id, openai_tool_stream *ts, const char *raw, size_t raw_len) { const char *tag_end = memchr(raw + ts->parse_pos, '>', raw_len - ts->parse_pos); if (!tag_end) return true; char *tag = xstrndup(raw + ts->parse_pos, (size_t)(tag_end - (raw + ts->parse_pos) + 1)); char *name = dsml_attr(tag, "name"); char *is_string = dsml_attr(tag, "string"); free(tag); if (!name || !is_string) { free(name); free(is_string); return openai_tool_stream_fail(ts); } bool string_value = !strcmp(is_string, "true"); bool ok = openai_tool_emit_param_prefix(fd, r, id, ts, name, string_value); free(name); free(is_string); if (!ok) return false; ts->param_is_string = string_value; ts->parse_pos = (size_t)(tag_end - raw) + 1; ts->state = DSML_TOOL_PARAM_VALUE; return true; } static bool openai_tool_finish_param(int fd, const request *r, const char *id, openai_tool_stream *ts, const char *raw, size_t value_end) { if (value_end > ts->parse_pos) { bool ok = ts->param_is_string ? openai_tool_emit_string_value(fd, r, id, ts, raw + ts->parse_pos, value_end - ts->parse_pos) : openai_tool_emit_args_fragment(fd, r, id, ts, raw + ts->parse_pos, value_end - ts->parse_pos); if (!ok) return false; } if (ts->param_is_string && !openai_tool_emit_args_fragment(fd, r, id, ts, "\"", 1)) return false; ts->parse_pos = value_end + strlen(ts->param_end); ts->state = DSML_TOOL_BETWEEN_PARAMS; return true; } static bool openai_tool_stream_update(int fd, server *s, const request *r, const char *id, openai_tool_stream *ts, const char *raw, size_t raw_len) { while (ts->active && ts->parse_pos < raw_len) { if (ts->state == DSML_TOOL_BETWEEN_INVOKES) { while (ts->parse_pos < raw_len && isspace((unsigned char)raw[ts->parse_pos])) ts->parse_pos++; if (ts->parse_pos >= raw_len) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->tool_calls_end)) { ts->parse_pos += strlen(ts->tool_calls_end); ts->active = false; ts->state = DSML_TOOL_DONE; return true; } if (raw_partial_any(raw, raw_len, ts->parse_pos, ts->tool_calls_end, ts->invoke_start)) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->invoke_start)) { size_t before_pos = ts->parse_pos; dsml_tool_stream_state before_state = ts->state; if (!openai_tool_start_invoke(fd, s, r, id, ts, raw, raw_len)) return false; if (ts->parse_pos == before_pos && ts->state == before_state) return true; continue; } return openai_tool_stream_fail(ts); } if (ts->state == DSML_TOOL_BETWEEN_PARAMS) { while (ts->parse_pos < raw_len && isspace((unsigned char)raw[ts->parse_pos])) ts->parse_pos++; if (ts->parse_pos >= raw_len) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->invoke_end)) { if (ts->args_open && !openai_tool_emit_args_fragment(fd, r, id, ts, "}", 1)) return false; ts->args_open = false; ts->parse_pos += strlen(ts->invoke_end); ts->index++; ts->state = DSML_TOOL_BETWEEN_INVOKES; continue; } if (raw_partial_any(raw, raw_len, ts->parse_pos, ts->invoke_end, ts->param_start)) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->param_start)) { size_t before_pos = ts->parse_pos; dsml_tool_stream_state before_state = ts->state; if (!openai_tool_start_param(fd, r, id, ts, raw, raw_len)) return false; if (ts->parse_pos == before_pos && ts->state == before_state) return true; continue; } return openai_tool_stream_fail(ts); } if (ts->state == DSML_TOOL_PARAM_VALUE) { const char *end = find_lit_bounded(raw + ts->parse_pos, raw_len - ts->parse_pos, ts->param_end); if (end) { if (!openai_tool_finish_param(fd, r, id, ts, raw, (size_t)(end - raw))) return false; continue; } size_t limit = tool_param_value_stream_safe_len(raw, ts->parse_pos, raw_len, ts->param_end, ts->param_is_string); if (limit > ts->parse_pos) { bool ok = ts->param_is_string ? openai_tool_emit_string_value(fd, r, id, ts, raw + ts->parse_pos, limit - ts->parse_pos) : openai_tool_emit_args_fragment(fd, r, id, ts, raw + ts->parse_pos, limit - ts->parse_pos); if (!ok) return false; ts->parse_pos = limit; } return true; } return true; } return true; } static bool openai_sse_stream_update(int fd, server *s, const request *r, const char *id, openai_stream *st, const char *raw, size_t raw_len, bool final) { if (!st->active || !raw) return true; if (st->mode == OPENAI_STREAM_THINKING) { if (!st->checked_think_prefix) { const char *open = ""; const size_t open_len = strlen(open); if (raw_len < open_len && !strncmp(raw, open, raw_len) && !final) { return true; } if (raw_len >= open_len && !strncmp(raw, open, open_len)) { st->emit_pos = open_len; } st->checked_think_prefix = true; } const char *close = strstr(raw + st->emit_pos, ""); const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; const bool tool_before_close = tool && (!close || tool < close); const bool complete_tool = tool_before_close && find_any_tool_end(tool) != NULL; size_t limit; if (tool_before_close) { limit = trim_tool_separator_ws(raw, st->emit_pos, (size_t)(tool - raw)); } else if (close) { limit = (size_t)(close - raw); } else if (final) { limit = raw_len; } else { const size_t hold = strlen("") - 1; limit = raw_len > hold ? raw_len - hold : st->emit_pos; limit = utf8_stream_safe_len(raw, st->emit_pos, limit, false); } if (limit > st->emit_pos) { if (!sse_chat_delta_n(fd, r, id, "reasoning_content", raw + st->emit_pos, limit - st->emit_pos)) return false; st->sent_reasoning = true; st->emit_pos = limit; } if (tool_before_close) { if (complete_tool) { st->emit_pos = (size_t)(tool - raw); st->mode = OPENAI_STREAM_SUPPRESS; } return true; } if (close) { st->emit_pos = (size_t)(close - raw) + strlen(""); st->mode = OPENAI_STREAM_TEXT; } else if (final) { st->mode = OPENAI_STREAM_SUPPRESS; return true; } else { return true; } } if (st->mode == OPENAI_STREAM_TEXT) { if (st->guard_second_reasoning) { const char *close = strstr(raw + st->emit_pos, ""); const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; if (close && (!tool || close < tool)) { const size_t limit = (size_t)(close - raw); if (limit > st->emit_pos && !sse_chat_delta_n(fd, r, id, "reasoning_content", raw + st->emit_pos, limit - st->emit_pos)) return false; if (limit > st->emit_pos) st->sent_reasoning = true; st->emit_pos = limit + strlen(""); st->guard_second_reasoning = false; } else if (!tool && !final) { return true; } else { st->guard_second_reasoning = false; } } const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; size_t limit = text_stream_safe_limit(raw, st->emit_pos, raw_len, r->has_tools, final); if (limit > st->emit_pos) { if (!sse_chat_delta_n(fd, r, id, "content", raw + st->emit_pos, limit - st->emit_pos)) return false; st->sent_content = true; st->emit_pos = limit; } if (tool) { st->emit_pos = (size_t)(tool - raw); if (openai_tool_stream_init(&st->tool, raw, raw_len, st->emit_pos)) { st->mode = OPENAI_STREAM_TOOL; } else { st->mode = OPENAI_STREAM_SUPPRESS; } } else if (final) { st->mode = OPENAI_STREAM_SUPPRESS; } } if (st->mode == OPENAI_STREAM_TOOL) { if (!openai_tool_stream_update(fd, s, r, id, &st->tool, raw, raw_len)) return false; if (!st->tool.active) st->mode = OPENAI_STREAM_SUPPRESS; } return true; } static bool openai_sse_finish_live(int fd, server *s, const request *r, const char *id, openai_stream *st, const char *raw, size_t raw_len, const tool_calls *calls, const char *finish, int prompt_tokens, int completion_tokens) { if (!openai_sse_stream_update(fd, s, r, id, st, raw, raw_len, true)) return false; buf b = {0}; long now = (long)time(NULL); if (calls && calls->len && !st->tool.emitted_any) { buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":"); append_tool_call_deltas_json(&b, calls, id, &r->tool_orders); buf_puts(&b, "},\"finish_reason\":null}]}\n\n"); } buf_printf(&b, "data: {\"id\":\"%s\",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":"); json_escape(&b, finish); buf_puts(&b, "}]}\n\n"); bool ok = send_all(fd, b.ptr, b.len) && sse_done(fd, r, id, prompt_tokens, completion_tokens); buf_free(&b); return ok; } static bool request_uses_openai_live_stream(const request *r) { return r->stream && r->api == API_OPENAI && r->kind == REQ_CHAT; } static bool request_uses_responses_live_stream(const request *r) { return r->stream && r->api == API_RESPONSES && r->kind == REQ_CHAT; } static bool request_uses_structured_stream(const request *r) { return r->stream && (r->api == API_ANTHROPIC || r->api == API_RESPONSES || request_uses_openai_live_stream(r)); } /* Codex' Responses API uses 24-hex suffixes for response/item ids. Prefix * controls the variant (resp_, rs_, msg_, fc_) so each event references a * stable identifier across output_item.added / .done. */ static void responses_random_id(char *dst, size_t dstlen, const char *prefix) { unsigned char bytes[12]; size_t pos = snprintf(dst, dstlen, "%s", prefix); if (pos >= dstlen) return; static uint64_t fallback_ctr; if (!random_bytes(bytes, sizeof(bytes))) { uint64_t a = ((uint64_t)time(NULL) << 32) ^ (uint64_t)getpid(); uint64_t b = ++fallback_ctr ^ (uint64_t)(uintptr_t)dst; memcpy(bytes, &a, sizeof(a)); memcpy(bytes + sizeof(a), &b, sizeof(uint32_t)); } static const char hex[] = "0123456789abcdef"; for (size_t i = 0; i < sizeof(bytes) && pos + 2 < dstlen; i++) { dst[pos++] = hex[bytes[i] >> 4]; dst[pos++] = hex[bytes[i] & 15]; } dst[pos] = '\0'; } typedef enum { RESP_STREAM_THINKING, RESP_STREAM_TEXT, RESP_STREAM_SUPPRESS, } responses_stream_mode; typedef struct { responses_stream_mode mode; size_t emit_pos; bool active; bool checked_think_prefix; bool reasoning_item_opened; bool reasoning_item_closed; bool reasoning_summary_started; bool reasoning_closed_naturally; bool message_item_opened; bool message_text_part_open; bool message_item_closed; bool reasoning_emitted_any; bool message_emitted_any; buf reasoning_text; buf message_text; char response_id[40]; char reasoning_id[40]; char message_id[40]; int reasoning_index; /* output_index of the reasoning item (0 if present) */ int message_index; /* output_index of the assistant message item */ int next_output_index; /* monotonic counter for upcoming output items */ int sequence; /* monotonic per-event sequence_number Codex consumes */ } responses_stream; static void responses_stream_init(const request *r, responses_stream *st) { memset(st, 0, sizeof(*st)); st->mode = ds4_think_mode_enabled(r->think_mode) ? RESP_STREAM_THINKING : RESP_STREAM_TEXT; responses_random_id(st->response_id, sizeof(st->response_id), "resp_"); responses_random_id(st->reasoning_id, sizeof(st->reasoning_id), "rs_"); responses_random_id(st->message_id, sizeof(st->message_id), "msg_"); st->reasoning_index = -1; st->message_index = -1; } static void responses_stream_free(responses_stream *st) { if (!st) return; buf_free(&st->reasoning_text); buf_free(&st->message_text); } /* Codex parses an explicit sequence_number on every Responses event for * ordering and reconnect resilience. We inject it after the `{"type":...` head * so emitters can stay readable while still producing the wire shape Codex * expects. */ static bool responses_sse_emit_event(int fd, responses_stream *st, const char *body) { buf b = {0}; buf_puts(&b, "data: "); /* body always starts with `{"type":"..."`. We splice in sequence_number * after the closing quote of that string so every event has it as the * second field. */ const char *type_close = NULL; if (body[0] == '{') { const char *p = body + 1; /* Skip the literal `"type":` then the value string. */ if (!strncmp(p, "\"type\":\"", 8)) { const char *q = p + 8; while (*q && *q != '"') { if (*q == '\\' && q[1]) q += 2; else q++; } if (*q == '"') type_close = q + 1; } } if (type_close) { size_t head_len = (size_t)(type_close - body); buf_append(&b, body, head_len); buf_printf(&b, ",\"sequence_number\":%d", st->sequence++); buf_puts(&b, type_close); } else { buf_puts(&b, body); } buf_puts(&b, "\n\n"); bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } static bool responses_sse_created(int fd, const request *r, responses_stream *st, long created_at) { buf b = {0}; buf_printf(&b, "{\"type\":\"response.created\",\"response\":{\"id\":\"%s\"," "\"object\":\"response\",\"created_at\":%ld,\"status\":\"in_progress\"," "\"model\":", st->response_id, created_at); json_escape(&b, r->model); buf_puts(&b, ",\"output\":[]}}"); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_reasoning_added(int fd, responses_stream *st) { buf b = {0}; buf_printf(&b, "{\"type\":\"response.output_item.added\",\"output_index\":%d," "\"item\":{\"id\":\"%s\",\"type\":\"reasoning\",\"status\":\"in_progress\"," "\"summary\":[]}}", st->reasoning_index, st->reasoning_id); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_reasoning_summary_part_added(int fd, responses_stream *st) { buf b = {0}; buf_printf(&b, "{\"type\":\"response.reasoning_summary_part.added\"," "\"item_id\":\"%s\",\"output_index\":%d,\"summary_index\":0," "\"part\":{\"type\":\"summary_text\",\"text\":\"\"}}", st->reasoning_id, st->reasoning_index); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_reasoning_delta(int fd, responses_stream *st, const char *text, size_t len) { if (len == 0) return true; buf b = {0}; buf_printf(&b, "{\"type\":\"response.reasoning_summary_text.delta\"," "\"item_id\":\"%s\",\"output_index\":%d,\"summary_index\":0,\"delta\":", st->reasoning_id, st->reasoning_index); json_escape_n(&b, text, len); buf_putc(&b, '}'); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static const char *responses_item_status_for_finish(const char *finish) { if (finish && (!strcmp(finish, "length") || !strcmp(finish, "error"))) return "incomplete"; return "completed"; } static bool responses_sse_reasoning_done(int fd, responses_stream *st, const char *finish) { /* If the stream terminates before `` was actually observed the * reasoning item is partial — regardless of why generation stopped (EOS, * stop sequence, tool_calls, length, error). Force the item to incomplete * so a client replay rejects it instead of feeding unfinished hidden state * back as completed history. */ (void)finish; const char *item_status = st->reasoning_closed_naturally ? "completed" : "incomplete"; /* Mirror the message-item close sequence: emit summary_text.done + * summary_part.done before the output_item.done so clients that key off * part lifecycle don't see a dangling open summary part. */ buf b = {0}; buf_printf(&b, "{\"type\":\"response.reasoning_summary_text.done\"," "\"item_id\":\"%s\",\"output_index\":%d,\"summary_index\":0,\"text\":", st->reasoning_id, st->reasoning_index); json_escape_n(&b, st->reasoning_text.ptr ? st->reasoning_text.ptr : "", st->reasoning_text.len); buf_putc(&b, '}'); bool ok = responses_sse_emit_event(fd, st, b.ptr); if (!ok) { buf_free(&b); return false; } if (st->reasoning_summary_started) { buf_free(&b); buf_printf(&b, "{\"type\":\"response.reasoning_summary_part.done\"," "\"item_id\":\"%s\",\"output_index\":%d,\"summary_index\":0," "\"part\":{\"type\":\"summary_text\",\"text\":", st->reasoning_id, st->reasoning_index); json_escape_n(&b, st->reasoning_text.ptr ? st->reasoning_text.ptr : "", st->reasoning_text.len); buf_puts(&b, "}}"); ok = responses_sse_emit_event(fd, st, b.ptr); if (!ok) { buf_free(&b); return false; } } buf_free(&b); buf_printf(&b, "{\"type\":\"response.output_item.done\",\"output_index\":%d," "\"item\":{\"id\":\"%s\",\"type\":\"reasoning\",\"status\":\"%s\",\"summary\":[", st->reasoning_index, st->reasoning_id, item_status); if (st->reasoning_text.len) { buf_puts(&b, "{\"type\":\"summary_text\",\"text\":"); json_escape_n(&b, st->reasoning_text.ptr, st->reasoning_text.len); buf_putc(&b, '}'); } buf_puts(&b, "]}}"); ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_message_added(int fd, responses_stream *st) { buf b = {0}; buf_printf(&b, "{\"type\":\"response.output_item.added\",\"output_index\":%d," "\"item\":{\"id\":\"%s\",\"type\":\"message\",\"status\":\"in_progress\"," "\"role\":\"assistant\",\"content\":[]}}", st->message_index, st->message_id); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_message_text_part_added(int fd, responses_stream *st) { buf b = {0}; buf_printf(&b, "{\"type\":\"response.content_part.added\"," "\"item_id\":\"%s\",\"output_index\":%d,\"content_index\":0," "\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[]}}", st->message_id, st->message_index); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_output_text_delta(int fd, responses_stream *st, const char *text, size_t len) { if (len == 0) return true; buf b = {0}; buf_printf(&b, "{\"type\":\"response.output_text.delta\"," "\"item_id\":\"%s\",\"output_index\":%d,\"content_index\":0,\"delta\":", st->message_id, st->message_index); json_escape_n(&b, text, len); buf_putc(&b, '}'); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } static bool responses_sse_message_done(int fd, responses_stream *st, const char *finish) { const char *item_status = responses_item_status_for_finish(finish); buf b = {0}; buf_printf(&b, "{\"type\":\"response.output_text.done\"," "\"item_id\":\"%s\",\"output_index\":%d,\"content_index\":0,\"text\":", st->message_id, st->message_index); json_escape_n(&b, st->message_text.ptr ? st->message_text.ptr : "", st->message_text.len); buf_putc(&b, '}'); bool ok = responses_sse_emit_event(fd, st, b.ptr); if (!ok) { buf_free(&b); return false; } buf_free(&b); buf_printf(&b, "{\"type\":\"response.content_part.done\"," "\"item_id\":\"%s\",\"output_index\":%d,\"content_index\":0," "\"part\":{\"type\":\"output_text\",\"text\":", st->message_id, st->message_index); json_escape_n(&b, st->message_text.ptr ? st->message_text.ptr : "", st->message_text.len); buf_puts(&b, ",\"annotations\":[]}}"); ok = responses_sse_emit_event(fd, st, b.ptr); if (!ok) { buf_free(&b); return false; } buf_free(&b); buf_printf(&b, "{\"type\":\"response.output_item.done\",\"output_index\":%d," "\"item\":{\"id\":\"%s\",\"type\":\"message\",\"status\":\"%s\"," "\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":", st->message_index, st->message_id, item_status); json_escape_n(&b, st->message_text.ptr ? st->message_text.ptr : "", st->message_text.len); buf_puts(&b, ",\"annotations\":[]}]}}"); ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } /* Item identity per tool call must be stable across added/done/completed. */ typedef struct { char fc_id[40]; char call_id[64]; bool is_custom; int output_index; } responses_tool_item; static bool responses_tool_call_is_tool_search(const tool_call *tc, const tool_schema_order *order) { return tc && tc->name && !strcmp(tc->name, "tool_search") && (!order || order->responses_tool_search); } /* The internal tool_call doesn't track whether it came from a function_call or * a custom_tool_call (or what tool kind is registered). For round-trip * correctness with the rare custom_tool_call clients, we preserve any provided * call_id verbatim and pre-assign a stable fc_id; the discriminator currently * defaults to function_call because Codex CLI registers all its tools as * function tools. */ static void responses_tool_items_build(responses_tool_item **out, const tool_calls *calls, int starting_output_index) { *out = NULL; if (!calls || calls->len == 0) return; responses_tool_item *items = xmalloc((size_t)calls->len * sizeof(*items)); for (int i = 0; i < calls->len; i++) { memset(&items[i], 0, sizeof(items[i])); responses_random_id(items[i].fc_id, sizeof(items[i].fc_id), "fc_"); if (calls->v[i].id && calls->v[i].id[0]) { snprintf(items[i].call_id, sizeof(items[i].call_id), "%s", calls->v[i].id); } else { responses_random_id(items[i].call_id, sizeof(items[i].call_id), "call_"); } items[i].is_custom = false; items[i].output_index = starting_output_index + i; } *out = items; } static void responses_append_function_call_item(buf *b, const tool_call *tc, const responses_tool_item *item, const char *item_status, bool with_args, const tool_schema_orders *orders) { const tool_schema_order *order = tool_schema_orders_find(orders, tc->name); if (responses_tool_call_is_tool_search(tc, order)) { buf_printf(b, "{\"id\":\"%s\",\"type\":\"tool_search_call\",\"status\":\"%s\"," "\"call_id\":\"%s\",\"execution\":\"client\",\"arguments\":", item->fc_id, item_status, item->call_id); if (with_args) append_json_object_or_empty(b, tc->arguments); else buf_puts(b, "{}"); buf_putc(b, '}'); return; } const char *item_type = item->is_custom ? "custom_tool_call" : "function_call"; const char *body_field = item->is_custom ? "input" : "arguments"; buf_printf(b, "{\"id\":\"%s\",\"type\":\"%s\",\"status\":\"%s\",\"name\":", item->fc_id, item_type, item_status); json_escape(b, order && order->wire_name ? order->wire_name : (tc->name ? tc->name : "")); if (order && order->namespace) { buf_puts(b, ",\"namespace\":"); json_escape(b, order->namespace); } buf_puts(b, ",\"call_id\":"); json_escape(b, item->call_id); buf_printf(b, ",\"%s\":", body_field); if (!with_args) { buf_puts(b, "\"\""); } else if (item->is_custom) { json_escape(b, tc->arguments ? tc->arguments : ""); } else { append_json_object_string(b, tc->arguments); } buf_putc(b, '}'); } static bool responses_sse_function_call_event(int fd, responses_stream *st, const tool_call *tc, const responses_tool_item *item, const tool_schema_orders *orders, const char *finish, bool done) { /* The added event marks a tool call as in_progress per the Responses * lifecycle; only output_item.done (and the terminal response output) * carry the final completed / incomplete status. The added item ships with * an empty arguments string so clients that accumulate via * function_call_arguments.delta + .done don't end up with doubled JSON. */ const char *item_status = done ? responses_item_status_for_finish(finish) : "in_progress"; buf b = {0}; buf_printf(&b, "{\"type\":\"response.output_item.%s\",\"output_index\":%d,\"item\":", done ? "done" : "added", item->output_index); responses_append_function_call_item(&b, tc, item, item_status, done, orders); buf_putc(&b, '}'); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } /* Stream function-call arguments as a single delta + done, since DS4 generates * the whole DSML invoke as one unit before the worker decides which tool was * called. Clients that follow the OpenAI Responses lifecycle expect both * events between output_item.added (in_progress) and output_item.done. */ static bool responses_sse_function_call_arguments_done(int fd, responses_stream *st, const tool_call *tc, const responses_tool_item *item, const tool_schema_orders *orders) { const tool_schema_order *order = tool_schema_orders_find(orders, tc->name); if (item->is_custom || responses_tool_call_is_tool_search(tc, order)) return true; buf args = {0}; append_json_object_string(&args, tc->arguments); buf b = {0}; buf_printf(&b, "{\"type\":\"response.function_call_arguments.delta\"," "\"item_id\":\"%s\",\"output_index\":%d,\"delta\":", item->fc_id, item->output_index); buf_append(&b, args.ptr ? args.ptr : "\"\"", args.ptr ? args.len : 2); buf_putc(&b, '}'); bool ok = responses_sse_emit_event(fd, st, b.ptr); if (!ok) { buf_free(&b); buf_free(&args); return false; } buf_free(&b); buf_printf(&b, "{\"type\":\"response.function_call_arguments.done\"," "\"item_id\":\"%s\",\"output_index\":%d,\"name\":", item->fc_id, item->output_index); json_escape(&b, order && order->wire_name ? order->wire_name : (tc->name ? tc->name : "")); if (order && order->namespace) { buf_puts(&b, ",\"namespace\":"); json_escape(&b, order->namespace); } buf_puts(&b, ",\"arguments\":"); buf_append(&b, args.ptr ? args.ptr : "\"\"", args.ptr ? args.len : 2); buf_putc(&b, '}'); ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); buf_free(&args); return ok; } static const char *responses_status_for_finish(const char *finish) { if (finish && !strcmp(finish, "length")) return "incomplete"; if (finish && !strcmp(finish, "error")) return "failed"; return "completed"; } static void append_responses_usage_json(buf *b, const request *r, int input_tokens, int output_tokens) { int cached_tokens = r ? r->cache_read_tokens : 0; int cache_write_tokens = r ? r->cache_write_tokens : 0; cached_tokens = clamp_usage_tokens(cached_tokens, input_tokens); cache_write_tokens = clamp_usage_tokens(cache_write_tokens, input_tokens - cached_tokens); buf_printf(b, "{\"input_tokens\":%d,\"input_tokens_details\":{\"cached_tokens\":%d,\"cache_write_tokens\":%d}," "\"output_tokens\":%d,\"output_tokens_details\":{\"reasoning_tokens\":0}," "\"total_tokens\":%d}", input_tokens, cached_tokens, cache_write_tokens, output_tokens, input_tokens + output_tokens); } static bool responses_sse_completed(int fd, const request *r, responses_stream *st, const tool_calls *calls, const responses_tool_item *tool_items, const char *finish, int prompt_tokens, int completion_tokens, long created_at) { /* Codex routes terminal behaviour off the event type, not response.status. * Decide here so clients see response.failed / response.incomplete instead * of a "completed" wrapper marked failed in a sub-field. */ const char *event_type = "response.completed"; if (finish && !strcmp(finish, "error")) event_type = "response.failed"; else if (finish && !strcmp(finish, "length")) event_type = "response.incomplete"; const char *status = responses_status_for_finish(finish); buf b = {0}; buf_printf(&b, "{\"type\":\"%s\",\"response\":{\"id\":\"%s\"," "\"object\":\"response\",\"created_at\":%ld,\"status\":\"%s\",\"model\":", event_type, st->response_id, created_at, status); json_escape(&b, r->model); if (!strcmp(event_type, "response.failed")) { buf_puts(&b, ",\"error\":{\"code\":\"server_error\"," "\"message\":\"generation failed\"}"); } else if (!strcmp(event_type, "response.incomplete")) { buf_puts(&b, ",\"incomplete_details\":{\"reason\":\"max_tokens\"}"); } const char *item_status = responses_item_status_for_finish(finish); buf_puts(&b, ",\"output\":["); bool wrote = false; if (st->reasoning_emitted_any) { /* Match responses_sse_reasoning_done: if the stream stopped before * , the reasoning item is partial regardless of the * response-level finish status, so replay must reject it. */ const char *reasoning_status = st->reasoning_closed_naturally ? "completed" : "incomplete"; buf_printf(&b, "{\"id\":\"%s\",\"type\":\"reasoning\",\"status\":\"%s\",\"summary\":[", st->reasoning_id, reasoning_status); if (st->reasoning_text.len) { buf_puts(&b, "{\"type\":\"summary_text\",\"text\":"); json_escape_n(&b, st->reasoning_text.ptr, st->reasoning_text.len); buf_putc(&b, '}'); } buf_puts(&b, "]}"); wrote = true; } if (st->message_emitted_any) { if (wrote) buf_putc(&b, ','); buf_printf(&b, "{\"id\":\"%s\",\"type\":\"message\",\"status\":\"%s\"," "\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":", st->message_id, item_status); json_escape_n(&b, st->message_text.ptr ? st->message_text.ptr : "", st->message_text.len); buf_puts(&b, ",\"annotations\":[]}]}"); wrote = true; } if (calls && tool_items) { for (int i = 0; i < calls->len; i++) { if (wrote) buf_putc(&b, ','); responses_append_function_call_item(&b, &calls->v[i], &tool_items[i], item_status, true, &r->tool_orders); wrote = true; } } buf_putc(&b, ']'); buf_puts(&b, ",\"usage\":"); append_responses_usage_json(&b, r, prompt_tokens, completion_tokens); buf_puts(&b, "}}"); bool ok = responses_sse_emit_event(fd, st, b.ptr); buf_free(&b); return ok; } /* Responses streaming consumes the same raw token text the OpenAI live stream * consumes: ... is reasoning, anything before the tool-call * marker is output text. Tool-call argument deltas are not surfaced because * Codex' SSE parser only ingests function_call items via output_item.done. */ static bool responses_sse_stream_update(int fd, const request *r, responses_stream *st, const char *raw, size_t raw_len, bool final) { if (!st->active || !raw) return true; /* The client only sees reasoning if it explicitly opted in via * reasoning.summary. Otherwise we still need to walk past ... * to find the user-visible text, but we suppress the per-chunk emission. */ const bool emit_reasoning = r->reasoning_summary_emit; if (st->mode == RESP_STREAM_THINKING) { if (!st->checked_think_prefix) { /* The chat template ends the prompt with the literal `` (or * `` when thinking is off), so generation usually starts * mid-reasoning rather than with the open tag. If the model does * happen to repeat `` we skip it; otherwise start from * position 0. The earlier "no-think-prefix => switch to TEXT" * shortcut here was incorrect: it leaked reasoning to clients as * regular output_text because the model was already inside the * think block when it produced its first token. The actual * mode change to TEXT happens only when `` is observed. */ const char *open = ""; const size_t open_len = strlen(open); if (raw_len < open_len && !strncmp(raw, open, raw_len) && !final) { return true; } if (raw_len >= open_len && !strncmp(raw, open, open_len)) { st->emit_pos = open_len; } st->checked_think_prefix = true; } const char *close = strstr(raw + st->emit_pos, ""); const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; const bool tool_before_close = tool && (!close || tool < close); const bool complete_tool = tool_before_close && find_any_tool_end(tool) != NULL; size_t limit; if (tool_before_close) { limit = trim_tool_separator_ws(raw, st->emit_pos, (size_t)(tool - raw)); } else if (close) { limit = (size_t)(close - raw); } else if (final) { limit = raw_len; } else { const size_t hold = strlen("") - 1; limit = raw_len > hold ? raw_len - hold : st->emit_pos; limit = utf8_stream_safe_len(raw, st->emit_pos, limit, false); } if (limit > st->emit_pos) { if (emit_reasoning) { if (!st->reasoning_item_opened) { st->reasoning_index = st->next_output_index++; if (!responses_sse_reasoning_added(fd, st)) return false; st->reasoning_item_opened = true; } if (!st->reasoning_summary_started) { if (!responses_sse_reasoning_summary_part_added(fd, st)) return false; st->reasoning_summary_started = true; } if (!responses_sse_reasoning_delta(fd, st, raw + st->emit_pos, limit - st->emit_pos)) return false; buf_append(&st->reasoning_text, raw + st->emit_pos, limit - st->emit_pos); st->reasoning_emitted_any = true; } st->emit_pos = limit; } if (tool_before_close) { if (complete_tool) { st->emit_pos = (size_t)(tool - raw); st->mode = RESP_STREAM_SUPPRESS; } return true; } if (close) { st->emit_pos = (size_t)(close - raw) + strlen(""); st->mode = RESP_STREAM_TEXT; st->reasoning_closed_naturally = true; } else if (final) { st->mode = RESP_STREAM_SUPPRESS; return true; } else { return true; } } if (st->mode == RESP_STREAM_TEXT) { const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; size_t limit = text_stream_safe_limit(raw, st->emit_pos, raw_len, r->has_tools, final); if (limit > st->emit_pos) { if (!st->message_item_opened) { st->message_index = st->next_output_index++; if (!responses_sse_message_added(fd, st)) return false; st->message_item_opened = true; } if (!st->message_text_part_open) { if (!responses_sse_message_text_part_added(fd, st)) return false; st->message_text_part_open = true; } if (!responses_sse_output_text_delta(fd, st, raw + st->emit_pos, limit - st->emit_pos)) return false; buf_append(&st->message_text, raw + st->emit_pos, limit - st->emit_pos); st->message_emitted_any = true; st->emit_pos = limit; } if (tool) { st->emit_pos = (size_t)(tool - raw); st->mode = RESP_STREAM_SUPPRESS; } else if (final) { st->mode = RESP_STREAM_SUPPRESS; } } return true; } static bool responses_sse_finish_live(int fd, const request *r, responses_stream *st, const char *raw, size_t raw_len, const char *recovered_content, const tool_calls *calls, const char *finish, int prompt_tokens, int completion_tokens, long created_at) { if (!responses_sse_stream_update(fd, r, st, raw, raw_len, true)) return false; /* Close any half-open reasoning summary so the TUI knows the part ended * before we slot in any tool calls or completion. */ if (st->reasoning_item_opened && !st->reasoning_item_closed) { if (!responses_sse_reasoning_done(fd, st, finish)) return false; st->reasoning_item_closed = true; } /* Recovery path: when DSML tool parsing fails the worker promotes the entire * generation to assistant text. Streaming had already entered suppress mode * at the tool marker, so anything in raw[st->emit_pos..raw_len] never made * it to the client. Emit those bytes as additional output_text deltas so * what the client accumulates matches output_item.done and the terminal * response. We use the stream cursor instead of comparing against * recovered_content because the raw text can begin with `...` * which the streaming side consumed as reasoning, not message text. */ if (recovered_content && raw && st->emit_pos < raw_len) { const char *tail = raw + st->emit_pos; size_t tail_len = raw_len - st->emit_pos; if (!st->message_item_opened) { st->message_index = st->next_output_index++; if (!responses_sse_message_added(fd, st)) return false; st->message_item_opened = true; } if (!st->message_text_part_open) { if (!responses_sse_message_text_part_added(fd, st)) return false; st->message_text_part_open = true; } if (!responses_sse_output_text_delta(fd, st, tail, tail_len)) return false; buf_append(&st->message_text, tail, tail_len); st->message_emitted_any = true; st->emit_pos = raw_len; } if (st->message_item_opened && !st->message_item_closed) { if (!responses_sse_message_done(fd, st, finish)) return false; st->message_item_closed = true; } responses_tool_item *items = NULL; responses_tool_items_build(&items, calls, st->next_output_index); if (items && calls) st->next_output_index += calls->len; bool ok = true; if (items && calls) { for (int i = 0; i < calls->len && ok; i++) { ok = responses_sse_function_call_event(fd, st, &calls->v[i], &items[i], &r->tool_orders, finish, false); if (ok) ok = responses_sse_function_call_arguments_done(fd, st, &calls->v[i], &items[i], &r->tool_orders); if (ok) ok = responses_sse_function_call_event(fd, st, &calls->v[i], &items[i], &r->tool_orders, finish, true); } } if (ok) ok = responses_sse_completed(fd, r, st, calls, items, finish, prompt_tokens, completion_tokens, created_at); free(items); return ok; } static bool responses_final_response(int fd, bool enable_cors, const request *r, const char *id, const char *text, const char *reasoning, const tool_calls *calls, const char *finish, int prompt_tokens, int completion_tokens) { (void)id; char response_id[40], reasoning_id[40], message_id[40]; responses_random_id(response_id, sizeof(response_id), "resp_"); responses_random_id(reasoning_id, sizeof(reasoning_id), "rs_"); responses_random_id(message_id, sizeof(message_id), "msg_"); responses_tool_item *items = NULL; responses_tool_items_build(&items, calls, 0); long now = (long)time(NULL); const char *status = responses_status_for_finish(finish); const char *item_status = responses_item_status_for_finish(finish); buf b = {0}; buf_printf(&b, "{\"id\":\"%s\",\"object\":\"response\",\"created_at\":%ld,\"status\":\"%s\"," "\"model\":", response_id, now, status); json_escape(&b, r->model); if (finish && !strcmp(finish, "error")) { buf_puts(&b, ",\"error\":{\"code\":\"server_error\"," "\"message\":\"generation failed\"}"); } else if (finish && !strcmp(finish, "length")) { buf_puts(&b, ",\"incomplete_details\":{\"reason\":\"max_tokens\"}"); } buf_puts(&b, ",\"output\":["); bool wrote = false; if (reasoning && reasoning[0] && r->reasoning_summary_emit) { /* Non-streaming path runs after the worker has post-processed the * generation, so any reasoning here came from a parsed assistant turn * where was observed (otherwise the reasoning text would be * empty). Tag it with the response-level item_status which still flips * to incomplete/failed when finish is length/error. */ buf_printf(&b, "{\"id\":\"%s\",\"type\":\"reasoning\",\"status\":\"%s\"," "\"summary\":[{\"type\":\"summary_text\",\"text\":", reasoning_id, item_status); json_escape(&b, reasoning); buf_puts(&b, "}]}"); wrote = true; } if (text && text[0]) { if (wrote) buf_putc(&b, ','); buf_printf(&b, "{\"id\":\"%s\",\"type\":\"message\",\"status\":\"%s\"," "\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":", message_id, item_status); json_escape(&b, text); buf_puts(&b, ",\"annotations\":[]}]}"); wrote = true; } if (calls && items) { for (int i = 0; i < calls->len; i++) { if (wrote) buf_putc(&b, ','); responses_append_function_call_item(&b, &calls->v[i], &items[i], item_status, true, &r->tool_orders); wrote = true; } } buf_putc(&b, ']'); buf_puts(&b, ",\"usage\":"); append_responses_usage_json(&b, r, prompt_tokens, completion_tokens); buf_putc(&b, '}'); bool ok = http_response(fd, enable_cors, 200, "application/json", b.ptr); buf_free(&b); free(items); return ok; } static bool final_response(int fd, bool enable_cors, const request *r, const char *id, const char *text, const char *reasoning, const tool_calls *calls, const char *finish, int prompt_tokens, int completion_tokens) { buf b = {0}; long now = (long)time(NULL); if (r->kind == REQ_CHAT) { buf_printf(&b, "{\"id\":\"%s\",\"object\":\"chat.completion\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":"); json_escape(&b, text ? text : ""); if (reasoning && reasoning[0]) { buf_puts(&b, ",\"reasoning_content\":"); json_escape(&b, reasoning); } if (calls && calls->len) { buf_puts(&b, ",\"tool_calls\":"); append_tool_calls_json(&b, calls, id, &r->tool_orders); } buf_puts(&b, "},\"finish_reason\":"); json_escape(&b, finish); buf_puts(&b, "}],\"usage\":"); } else { buf_printf(&b, "{\"id\":\"%s\",\"object\":\"text_completion\",\"created\":%ld,\"model\":", id, now); json_escape(&b, r->model); buf_puts(&b, ",\"choices\":[{\"text\":"); json_escape(&b, text); buf_puts(&b, ",\"index\":0,\"finish_reason\":"); json_escape(&b, finish); buf_puts(&b, "}],\"usage\":"); } append_openai_usage_json(&b, r, prompt_tokens, completion_tokens); buf_puts(&b, "}\n"); bool ok = http_response(fd, enable_cors, 200, "application/json", b.ptr); buf_free(&b); return ok; } static const char *anthropic_stop_reason(const char *finish) { if (finish && !strcmp(finish, "tool_calls")) return "tool_use"; if (finish && !strcmp(finish, "length")) return "max_tokens"; return "end_turn"; } static void append_anthropic_tool_use(buf *b, const tool_call *tc, const char *id_prefix, int i, const tool_schema_orders *orders) { (void)orders; char idbuf[128]; snprintf(idbuf, sizeof(idbuf), "toolu_%s_%d", id_prefix, i); buf_puts(b, "{\"type\":\"tool_use\",\"id\":"); json_escape(b, tc->id && tc->id[0] ? tc->id : idbuf); buf_puts(b, ",\"name\":"); json_escape(b, tc->name ? tc->name : ""); buf_puts(b, ",\"input\":"); append_json_object_or_empty(b, tc->arguments); buf_putc(b, '}'); } static void append_anthropic_thinking(buf *b, const char *reasoning, const char *signature) { buf_puts(b, "{\"type\":\"thinking\",\"thinking\":"); json_escape(b, reasoning ? reasoning : ""); buf_puts(b, ",\"signature\":"); json_escape(b, signature ? signature : ""); buf_putc(b, '}'); } static void append_anthropic_content(buf *b, const char *text, const char *reasoning, const tool_calls *calls, const char *id_prefix, const tool_schema_orders *orders) { buf_putc(b, '['); bool wrote = false; bool wrote_after_thinking = false; if (reasoning && reasoning[0]) { append_anthropic_thinking(b, reasoning, id_prefix); wrote = true; } if (text && text[0]) { if (wrote) buf_putc(b, ','); buf_puts(b, "{\"type\":\"text\",\"text\":"); json_escape(b, text); buf_putc(b, '}'); wrote = true; wrote_after_thinking = true; } if (calls) { for (int i = 0; i < calls->len; i++) { if (wrote) buf_putc(b, ','); append_anthropic_tool_use(b, &calls->v[i], id_prefix, i, orders); wrote = true; wrote_after_thinking = true; } } if (!wrote || ((reasoning && reasoning[0]) && !wrote_after_thinking)) { if (wrote) buf_putc(b, ','); buf_puts(b, "{\"type\":\"text\",\"text\":\"\"}"); } buf_putc(b, ']'); } static void append_anthropic_usage_json(buf *b, const request *r, int prompt_tokens, int completion_tokens) { int cache_read_tokens = r ? r->cache_read_tokens : 0; int cache_write_tokens = r ? r->cache_write_tokens : 0; cache_read_tokens = clamp_usage_tokens(cache_read_tokens, prompt_tokens); cache_write_tokens = clamp_usage_tokens(cache_write_tokens, prompt_tokens - cache_read_tokens); int input_tokens = prompt_tokens - cache_read_tokens - cache_write_tokens; if (input_tokens < 0) input_tokens = 0; buf_printf(b, "{\"input_tokens\":%d,\"output_tokens\":%d," "\"cache_read_input_tokens\":%d,\"cache_creation_input_tokens\":%d}", input_tokens, completion_tokens, cache_read_tokens, cache_write_tokens); } static bool anthropic_final_response(int fd, bool enable_cors, const request *r, const char *id, const char *text, const char *reasoning, const tool_calls *calls, const char *finish, int prompt_tokens, int completion_tokens) { buf b = {0}; buf_printf(&b, "{\"id\":\"%s\",\"type\":\"message\",\"role\":\"assistant\",\"model\":", id); json_escape(&b, r->model); buf_puts(&b, ",\"content\":"); append_anthropic_content(&b, text, reasoning, calls, id, &r->tool_orders); buf_puts(&b, ",\"stop_reason\":"); json_escape(&b, anthropic_stop_reason(finish)); buf_puts(&b, ",\"stop_sequence\":null,\"usage\":"); append_anthropic_usage_json(&b, r, prompt_tokens, completion_tokens); buf_puts(&b, "}\n"); bool ok = http_response(fd, enable_cors, 200, "application/json", b.ptr); buf_free(&b); return ok; } static bool sse_event(int fd, const char *event, const char *data) { buf b = {0}; buf_puts(&b, "event: "); buf_puts(&b, event); buf_puts(&b, "\ndata: "); buf_puts(&b, data); buf_puts(&b, "\n\n"); bool ok = send_all(fd, b.ptr, b.len); buf_free(&b); return ok; } typedef enum { ANTH_STREAM_THINKING, ANTH_STREAM_TEXT, ANTH_STREAM_TOOL, ANTH_STREAM_SUPPRESS, } anthropic_stream_mode; typedef enum { ANTH_BLOCK_NONE, ANTH_BLOCK_THINKING, ANTH_BLOCK_TEXT, ANTH_BLOCK_TOOL, } anthropic_block_type; typedef struct { dsml_tool_stream_state state; const dsml_syntax *syn; size_t parse_pos; int index; bool active; bool emitted_any; bool args_open; bool first_param; bool param_is_string; char **ids; int ids_cap; } anthropic_tool_stream; /* Anthropic streaming uses the same sampled DSML bytes that will later be * parsed and remembered for exact continuation. This state is only a wire * projection: it turns an in-progress DSML block into content_block/tool_use * SSE events, and never rewrites the model-visible transcript or cache key. */ typedef struct { anthropic_stream_mode mode; anthropic_block_type open_block; int next_index; size_t emit_pos; bool active; bool checked_think_prefix; bool guard_second_reasoning; bool sent_thinking; bool sent_text; anthropic_tool_stream tool; } anthropic_stream; static bool anthropic_sse_start_live(int fd, const request *r, const char *id, int prompt_tokens, anthropic_stream *st) { buf b = {0}; json_escape(&b, r->model); char *model_json = buf_take(&b); buf_printf(&b, "{\"type\":\"message_start\",\"message\":{\"id\":\"%s\",\"type\":\"message\"," "\"role\":\"assistant\",\"model\":%s,\"content\":[],\"stop_reason\":null," "\"stop_sequence\":null,\"usage\":", id, model_json); append_anthropic_usage_json(&b, r, prompt_tokens, 0); buf_puts(&b, "}}"); bool ok = sse_event(fd, "message_start", b.ptr); buf_free(&b); free(model_json); memset(st, 0, sizeof(*st)); st->active = ok; st->mode = ds4_think_mode_enabled(r->think_mode) ? ANTH_STREAM_THINKING : ANTH_STREAM_TEXT; st->guard_second_reasoning = stream_needs_second_reasoning_guard(r); return ok; } static void anthropic_tool_stream_free(anthropic_tool_stream *ts) { if (!ts) return; for (int i = 0; i < ts->ids_cap; i++) free(ts->ids[i]); free(ts->ids); ts->ids = NULL; ts->ids_cap = 0; } static void anthropic_stream_free(anthropic_stream *st) { if (!st) return; anthropic_tool_stream_free(&st->tool); } static bool anthropic_tool_stream_has_id(const anthropic_tool_stream *ts, const char *id, int upto) { if (!ts || !id || !id[0]) return false; if (upto > ts->ids_cap) upto = ts->ids_cap; for (int i = 0; i < upto; i++) { if (ts->ids[i] && !strcmp(ts->ids[i], id)) return true; } return false; } static const char *anthropic_tool_stream_id(server *s, anthropic_tool_stream *ts, int index) { if (!ts || index < 0) return ""; if (index >= ts->ids_cap) { int old = ts->ids_cap; int cap = old ? old : 4; while (cap <= index) cap *= 2; ts->ids = xrealloc(ts->ids, (size_t)cap * sizeof(ts->ids[0])); memset(ts->ids + old, 0, (size_t)(cap - old) * sizeof(ts->ids[0])); ts->ids_cap = cap; } if (!ts->ids[index]) { char id[64]; for (;;) { random_tool_id(id, sizeof(id), API_ANTHROPIC); if (!anthropic_tool_stream_has_id(ts, id, index) && !tool_memory_has_id(s, id)) break; } ts->ids[index] = xstrdup(id); } return ts->ids[index]; } /* Text and thinking blocks have fixed JSON shapes. Tool blocks are opened by * name later, after the DSML invoke tag is complete, so they use a dedicated * opener instead of this helper. */ static bool anthropic_sse_open_block(int fd, anthropic_stream *st, anthropic_block_type type) { if (st->open_block == type) return true; if (st->open_block != ANTH_BLOCK_NONE) return false; buf b = {0}; if (type == ANTH_BLOCK_THINKING) { buf_printf(&b, "{\"type\":\"content_block_start\",\"index\":%d," "\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"," "\"signature\":\"\"}}", st->next_index); } else { buf_printf(&b, "{\"type\":\"content_block_start\",\"index\":%d," "\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", st->next_index); } bool ok = sse_event(fd, "content_block_start", b.ptr); buf_free(&b); if (ok) st->open_block = type; return ok; } static bool anthropic_sse_open_tool_block(int fd, anthropic_stream *st, const char *tool_id, const char *name) { if (st->open_block == ANTH_BLOCK_TOOL) return true; if (st->open_block != ANTH_BLOCK_NONE) return false; buf b = {0}; buf_printf(&b, "{\"type\":\"content_block_start\",\"index\":%d," "\"content_block\":{\"type\":\"tool_use\",\"id\":", st->next_index); json_escape(&b, tool_id ? tool_id : ""); buf_puts(&b, ",\"name\":"); json_escape(&b, name ? name : ""); buf_puts(&b, ",\"input\":{}}}"); bool ok = sse_event(fd, "content_block_start", b.ptr); buf_free(&b); if (ok) st->open_block = ANTH_BLOCK_TOOL; return ok; } static bool anthropic_sse_delta_live(int fd, const anthropic_stream *st, anthropic_block_type type, const char *text, size_t len) { if (len == 0) return true; buf b = {0}; if (type == ANTH_BLOCK_THINKING) { buf_printf(&b, "{\"type\":\"content_block_delta\",\"index\":%d," "\"delta\":{\"type\":\"thinking_delta\",\"thinking\":", st->next_index); json_escape_n(&b, text, len); buf_puts(&b, "}}"); } else { buf_printf(&b, "{\"type\":\"content_block_delta\",\"index\":%d," "\"delta\":{\"type\":\"text_delta\",\"text\":", st->next_index); json_escape_n(&b, text, len); buf_puts(&b, "}}"); } bool ok = sse_event(fd, "content_block_delta", b.ptr); buf_free(&b); return ok; } /* Anthropic's input_json_delta carries a fragment of a JSON object, encoded as * a JSON string. We stream exactly the same object that the final DSML parser * will build: an opening "{", quoted keys, raw JSON values or escaped string * contents, and the closing "}". */ static bool anthropic_sse_tool_delta_live(int fd, const anthropic_stream *st, const char *text, size_t len) { if (len == 0) return true; buf b = {0}; buf_printf(&b, "{\"type\":\"content_block_delta\",\"index\":%d," "\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":", st->next_index); json_escape_n(&b, text, len); buf_puts(&b, "}}"); bool ok = sse_event(fd, "content_block_delta", b.ptr); buf_free(&b); return ok; } static bool anthropic_sse_close_block_live(int fd, const char *id, anthropic_stream *st) { if (st->open_block == ANTH_BLOCK_NONE) return true; buf b = {0}; bool ok = true; if (st->open_block == ANTH_BLOCK_THINKING) { buf_printf(&b, "{\"type\":\"content_block_delta\",\"index\":%d," "\"delta\":{\"type\":\"signature_delta\",\"signature\":", st->next_index); json_escape(&b, id); buf_puts(&b, "}}"); ok = sse_event(fd, "content_block_delta", b.ptr); buf_free(&b); } if (ok) { buf_printf(&b, "{\"type\":\"content_block_stop\",\"index\":%d}", st->next_index); ok = sse_event(fd, "content_block_stop", b.ptr); buf_free(&b); } if (ok) { st->open_block = ANTH_BLOCK_NONE; st->next_index++; } return ok; } static bool anthropic_tool_emit_args_fragment(int fd, anthropic_stream *st, const char *text, size_t len) { return anthropic_sse_tool_delta_live(fd, st, text, len); } static bool anthropic_tool_emit_string_value(int fd, anthropic_stream *st, const char *text, size_t len) { if (len == 0) return true; char *raw = xstrndup(text, len); ds4_tool_text_unescape(raw, st->tool.syn->param_end); buf frag = {0}; json_escape_fragment_n(&frag, raw, strlen(raw)); bool ok = anthropic_tool_emit_args_fragment(fd, st, frag.ptr ? frag.ptr : "", frag.len); buf_free(&frag); free(raw); return ok; } static bool anthropic_tool_emit_param_prefix(int fd, anthropic_stream *st, const char *name, bool is_string) { anthropic_tool_stream *ts = &st->tool; buf frag = {0}; if (ts->first_param) ts->first_param = false; else buf_putc(&frag, ','); json_escape(&frag, name ? name : ""); buf_putc(&frag, ':'); if (is_string) buf_putc(&frag, '"'); bool ok = anthropic_tool_emit_args_fragment(fd, st, frag.ptr ? frag.ptr : "", frag.len); buf_free(&frag); return ok; } /* The parser below mirrors the OpenAI tool-delta parser but keeps Anthropic's * content-block lifecycle local. A callback abstraction would save lines, but * it would hide the different block/stop semantics that make this code easy to * audit when a client reports a streaming regression. */ static bool anthropic_tool_stream_init(anthropic_tool_stream *ts, const char *raw, size_t raw_len, size_t pos) { anthropic_tool_stream_free(ts); memset(ts, 0, sizeof(*ts)); ts->active = true; ts->state = DSML_TOOL_BETWEEN_INVOKES; for (size_t i = 0; i < sizeof(dsml_syntaxes) / sizeof(dsml_syntaxes[0]); i++) { const dsml_syntax *syn = &dsml_syntaxes[i]; if (raw_full_lit(raw, raw_len, pos, syn->tool_calls_start)) { ts->syn = syn; ts->parse_pos = pos + strlen(syn->tool_calls_start); return true; } } ts->active = false; ts->state = DSML_TOOL_ERROR; return false; } static bool anthropic_tool_stream_fail(anthropic_tool_stream *ts) { ts->active = false; ts->state = DSML_TOOL_ERROR; return true; } static bool anthropic_tool_start_invoke(int fd, server *s, anthropic_stream *st, const char *raw, size_t raw_len) { anthropic_tool_stream *ts = &st->tool; const char *tag_end = memchr(raw + ts->parse_pos, '>', raw_len - ts->parse_pos); if (!tag_end) return true; char *tag = xstrndup(raw + ts->parse_pos, (size_t)(tag_end - (raw + ts->parse_pos) + 1)); char *name = dsml_attr(tag, "name"); free(tag); if (!name) return anthropic_tool_stream_fail(ts); /* This id is already visible to the client. After final parsing, * apply_anthropic_stream_tool_ids() copies it into the parsed tool_call * before tool_memory_remember(), so the next tool_result can continue from * the live KV state instead of re-rendering canonical JSON. */ const char *tool_id = anthropic_tool_stream_id(s, ts, ts->index); bool ok = anthropic_sse_open_tool_block(fd, st, tool_id, name) && anthropic_tool_emit_args_fragment(fd, st, "{", 1); free(name); if (!ok) return false; ts->emitted_any = true; ts->args_open = true; ts->first_param = true; ts->parse_pos = (size_t)(tag_end - raw) + 1; ts->state = DSML_TOOL_BETWEEN_PARAMS; return true; } static bool anthropic_tool_start_param(int fd, anthropic_stream *st, const char *raw, size_t raw_len) { anthropic_tool_stream *ts = &st->tool; const char *tag_end = memchr(raw + ts->parse_pos, '>', raw_len - ts->parse_pos); if (!tag_end) return true; char *tag = xstrndup(raw + ts->parse_pos, (size_t)(tag_end - (raw + ts->parse_pos) + 1)); char *name = dsml_attr(tag, "name"); char *is_string = dsml_attr(tag, "string"); free(tag); if (!name || !is_string) { free(name); free(is_string); return anthropic_tool_stream_fail(ts); } bool string_value = !strcmp(is_string, "true"); bool ok = anthropic_tool_emit_param_prefix(fd, st, name, string_value); free(name); free(is_string); if (!ok) return false; ts->param_is_string = string_value; ts->parse_pos = (size_t)(tag_end - raw) + 1; ts->state = DSML_TOOL_PARAM_VALUE; return true; } static bool anthropic_tool_finish_param(int fd, anthropic_stream *st, const char *raw, size_t value_end) { anthropic_tool_stream *ts = &st->tool; if (value_end > ts->parse_pos) { bool ok = ts->param_is_string ? anthropic_tool_emit_string_value(fd, st, raw + ts->parse_pos, value_end - ts->parse_pos) : anthropic_tool_emit_args_fragment(fd, st, raw + ts->parse_pos, value_end - ts->parse_pos); if (!ok) return false; } if (ts->param_is_string && !anthropic_tool_emit_args_fragment(fd, st, "\"", 1)) return false; ts->parse_pos = value_end + strlen(ts->syn->param_end); ts->state = DSML_TOOL_BETWEEN_PARAMS; return true; } static bool anthropic_tool_stream_update(int fd, server *s, const char *id, anthropic_stream *st, const char *raw, size_t raw_len) { anthropic_tool_stream *ts = &st->tool; while (ts->active && ts->parse_pos < raw_len) { if (ts->state == DSML_TOOL_BETWEEN_INVOKES) { while (ts->parse_pos < raw_len && isspace((unsigned char)raw[ts->parse_pos])) ts->parse_pos++; if (ts->parse_pos >= raw_len) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->syn->tool_calls_end)) { ts->parse_pos += strlen(ts->syn->tool_calls_end); ts->active = false; ts->state = DSML_TOOL_DONE; return true; } if (raw_partial_any(raw, raw_len, ts->parse_pos, ts->syn->tool_calls_end, ts->syn->invoke_start)) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->syn->invoke_start)) { size_t before_pos = ts->parse_pos; dsml_tool_stream_state before_state = ts->state; if (!anthropic_tool_start_invoke(fd, s, st, raw, raw_len)) return false; if (ts->parse_pos == before_pos && ts->state == before_state) return true; continue; } return anthropic_tool_stream_fail(ts); } if (ts->state == DSML_TOOL_BETWEEN_PARAMS) { while (ts->parse_pos < raw_len && isspace((unsigned char)raw[ts->parse_pos])) ts->parse_pos++; if (ts->parse_pos >= raw_len) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->syn->invoke_end)) { if (ts->args_open && !anthropic_tool_emit_args_fragment(fd, st, "}", 1)) return false; ts->args_open = false; if (!anthropic_sse_close_block_live(fd, id, st)) return false; ts->parse_pos += strlen(ts->syn->invoke_end); ts->index++; ts->state = DSML_TOOL_BETWEEN_INVOKES; continue; } if (raw_partial_any(raw, raw_len, ts->parse_pos, ts->syn->invoke_end, ts->syn->param_start)) return true; if (raw_full_lit(raw, raw_len, ts->parse_pos, ts->syn->param_start)) { size_t before_pos = ts->parse_pos; dsml_tool_stream_state before_state = ts->state; if (!anthropic_tool_start_param(fd, st, raw, raw_len)) return false; if (ts->parse_pos == before_pos && ts->state == before_state) return true; continue; } return anthropic_tool_stream_fail(ts); } if (ts->state == DSML_TOOL_PARAM_VALUE) { const char *end = find_lit_bounded(raw + ts->parse_pos, raw_len - ts->parse_pos, ts->syn->param_end); if (end) { if (!anthropic_tool_finish_param(fd, st, raw, (size_t)(end - raw))) return false; continue; } size_t limit = tool_param_value_stream_safe_len(raw, ts->parse_pos, raw_len, ts->syn->param_end, ts->param_is_string); if (limit > ts->parse_pos) { bool ok = ts->param_is_string ? anthropic_tool_emit_string_value(fd, st, raw + ts->parse_pos, limit - ts->parse_pos) : anthropic_tool_emit_args_fragment(fd, st, raw + ts->parse_pos, limit - ts->parse_pos); if (!ok) return false; ts->parse_pos = limit; } return true; } return true; } return true; } static size_t text_stream_safe_limit(const char *raw, size_t start, size_t raw_len, bool has_tools, bool final) { if (raw_len <= start) return raw_len; size_t limit = raw_len; if (has_tools) { const char *tool = find_any_tool_start(raw + start); if (tool) { limit = trim_tool_separator_ws(raw, start, (size_t)(tool - raw)); return utf8_stream_safe_len(raw, start, limit, true); } if (!final) { /* Tool calls are hidden from the API client and returned as * structured tool_use/tool_calls blocks. The whitespace just * before the DSML marker is syntax too: if we stream it as * assistant text, the next client request sends it back and our * renderer adds the canonical "\n\n" separator again. Hold * trailing whitespace until a following non-whitespace byte proves * it is ordinary text, or until a tool marker proves it should be * dropped. */ while (limit > start && isspace((unsigned char)raw[limit - 1])) limit--; /* Also hold a partial '<...tool_calls...' marker that may be split * across generated tokens. */ const size_t max_marker = 80; size_t scan = raw_len - start > max_marker ? raw_len - max_marker : start; for (size_t i = raw_len; i > scan; i--) { if (raw[i - 1] == '<') { size_t marker = i - 1; if (marker < limit) limit = marker; break; } } limit = trim_tool_separator_ws(raw, start, limit); } } return utf8_stream_safe_len(raw, start, limit, final); } static bool anthropic_sse_stream_update(int fd, server *s, const request *r, const char *id, anthropic_stream *st, const char *raw, size_t raw_len, bool final) { if (!st->active || !raw) return true; if (st->mode == ANTH_STREAM_THINKING) { if (!st->checked_think_prefix) { const char *open = ""; const size_t open_len = strlen(open); if (raw_len < open_len && !strncmp(raw, open, raw_len) && !final) { return true; } if (raw_len >= open_len && !strncmp(raw, open, open_len)) { st->emit_pos = open_len; } st->checked_think_prefix = true; } const char *close = strstr(raw + st->emit_pos, ""); const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; const bool tool_before_close = tool && (!close || tool < close); const bool complete_tool = tool_before_close && find_any_tool_end(tool) != NULL; size_t limit; if (tool_before_close) { limit = trim_tool_separator_ws(raw, st->emit_pos, (size_t)(tool - raw)); } else if (close) { limit = (size_t)(close - raw); } else if (final) { limit = raw_len; } else { const size_t hold = strlen("") - 1; limit = raw_len > hold ? raw_len - hold : st->emit_pos; limit = utf8_stream_safe_len(raw, st->emit_pos, limit, false); } if (limit > st->emit_pos) { if (!anthropic_sse_open_block(fd, st, ANTH_BLOCK_THINKING)) return false; if (!anthropic_sse_delta_live(fd, st, ANTH_BLOCK_THINKING, raw + st->emit_pos, limit - st->emit_pos)) return false; st->sent_thinking = true; st->emit_pos = limit; } if (tool_before_close) { if (complete_tool) { if (!anthropic_sse_close_block_live(fd, id, st)) return false; st->emit_pos = (size_t)(tool - raw); st->mode = ANTH_STREAM_SUPPRESS; } return true; } if (close || final) { if (!anthropic_sse_close_block_live(fd, id, st)) return false; if (close) { st->emit_pos = (size_t)(close - raw) + strlen(""); st->mode = ANTH_STREAM_TEXT; } else { st->mode = ANTH_STREAM_SUPPRESS; return true; } } else { return true; } } if (st->mode == ANTH_STREAM_TEXT) { if (st->guard_second_reasoning) { const char *close = strstr(raw + st->emit_pos, ""); const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; if (close && (!tool || close < tool)) { const size_t limit = (size_t)(close - raw); if (limit > st->emit_pos) { if (!anthropic_sse_open_block(fd, st, ANTH_BLOCK_THINKING)) return false; if (!anthropic_sse_delta_live(fd, st, ANTH_BLOCK_THINKING, raw + st->emit_pos, limit - st->emit_pos)) return false; st->sent_thinking = true; } if (!anthropic_sse_close_block_live(fd, id, st)) return false; st->emit_pos = limit + strlen(""); st->guard_second_reasoning = false; } else if (!tool && !final) { return true; } else { st->guard_second_reasoning = false; } } const char *tool = r->has_tools ? find_any_tool_start(raw + st->emit_pos) : NULL; size_t limit = text_stream_safe_limit(raw, st->emit_pos, raw_len, r->has_tools, final); if (limit > st->emit_pos) { if (!anthropic_sse_open_block(fd, st, ANTH_BLOCK_TEXT)) return false; if (!anthropic_sse_delta_live(fd, st, ANTH_BLOCK_TEXT, raw + st->emit_pos, limit - st->emit_pos)) return false; st->sent_text = true; st->emit_pos = limit; } if (tool) { if (!anthropic_sse_close_block_live(fd, id, st)) return false; st->emit_pos = (size_t)(tool - raw); /* On normal token-by-token updates, switch from hidden text to a * live tool_use projection as soon as the DSML block starts. On * final catch-up from plain text, leave the block for the existing * final emitter so old non-incremental behavior stays unchanged. */ if (!final && anthropic_tool_stream_init(&st->tool, raw, raw_len, st->emit_pos)) { st->mode = ANTH_STREAM_TOOL; } else { st->mode = ANTH_STREAM_SUPPRESS; } } else if (final) { if (!anthropic_sse_close_block_live(fd, id, st)) return false; st->mode = ANTH_STREAM_SUPPRESS; } } if (st->mode == ANTH_STREAM_TOOL) { if (!anthropic_tool_stream_update(fd, s, id, st, raw, raw_len)) return false; if (!st->tool.active) st->mode = ANTH_STREAM_SUPPRESS; } return true; } static bool anthropic_sse_tool_blocks_live(int fd, const request *r, const char *id, anthropic_stream *st, const tool_calls *calls) { (void)r; if (!calls) return true; buf b = {0}; /* Tool calls completed by anthropic_tool_stream_update() have already * produced start/delta/stop events. Only emit the tail calls that were not * seen by the live projection, for example if the first DSML bytes only * become available during final flush. */ int already_streamed = st->tool.emitted_any ? st->tool.index : 0; if (already_streamed > calls->len) already_streamed = calls->len; for (int i = already_streamed; i < calls->len; i++, st->next_index++) { const tool_call *tc = &calls->v[i]; char idbuf[128]; snprintf(idbuf, sizeof(idbuf), "toolu_%s_%d", id, i); buf_printf(&b, "{\"type\":\"content_block_start\",\"index\":%d," "\"content_block\":{\"type\":\"tool_use\",\"id\":", st->next_index); json_escape(&b, tc->id && tc->id[0] ? tc->id : idbuf); buf_puts(&b, ",\"name\":"); json_escape(&b, tc->name ? tc->name : ""); buf_puts(&b, ",\"input\":{}}}"); bool ok = sse_event(fd, "content_block_start", b.ptr); buf_free(&b); if (!ok) return false; buf_printf(&b, "{\"type\":\"content_block_delta\",\"index\":%d," "\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":", st->next_index); append_json_object_string(&b, tc->arguments); buf_puts(&b, "}}"); ok = sse_event(fd, "content_block_delta", b.ptr); buf_free(&b); if (!ok) return false; buf_printf(&b, "{\"type\":\"content_block_stop\",\"index\":%d}", st->next_index); ok = sse_event(fd, "content_block_stop", b.ptr); buf_free(&b); if (!ok) return false; } return true; } static bool anthropic_sse_stop_live(int fd, const char *finish, int completion_tokens) { buf b = {0}; buf_puts(&b, "{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":"); json_escape(&b, anthropic_stop_reason(finish)); buf_puts(&b, ",\"stop_sequence\":null},\"usage\":{\"output_tokens\":"); buf_printf(&b, "%d}}", completion_tokens); bool ok = sse_event(fd, "message_delta", b.ptr); buf_free(&b); if (ok) ok = sse_event(fd, "message_stop", "{\"type\":\"message_stop\"}"); return ok; } static bool anthropic_sse_finish_live(int fd, server *s, const request *r, const char *id, anthropic_stream *st, const char *raw, size_t raw_len, const tool_calls *calls, const char *finish, int completion_tokens) { if (!anthropic_sse_stream_update(fd, s, r, id, st, raw, raw_len, true)) return false; if (st->sent_thinking && !st->sent_text && (!calls || calls->len == 0)) { if (!anthropic_sse_open_block(fd, st, ANTH_BLOCK_TEXT)) return false; if (!anthropic_sse_close_block_live(fd, id, st)) return false; } if (!anthropic_sse_tool_blocks_live(fd, r, id, st, calls)) return false; return anthropic_sse_stop_live(fd, finish, completion_tokens); } static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } static pthread_mutex_t server_log_mu = PTHREAD_MUTEX_INITIALIZER; static void server_log(ds4_log_type type, const char *fmt, ...) { time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); char ts[16]; strftime(ts, sizeof(ts), "%m%d %H:%M:%S", &tm); va_list ap; va_start(ap, fmt); va_list copy; va_copy(copy, ap); int n = vsnprintf(NULL, 0, fmt, copy); va_end(copy); pthread_mutex_lock(&server_log_mu); fprintf(stderr, "%s ", ts); if (n < 0) { ds4_log(stderr, type, "%s", fmt); } else { char *line = xmalloc((size_t)n + 1); vsnprintf(line, (size_t)n + 1, fmt, ap); ds4_log(stderr, type, "%s", line); free(line); } va_end(ap); fputc('\n', stderr); pthread_mutex_unlock(&server_log_mu); } typedef struct job job; typedef struct server_slot server_slot; typedef ds4_kvstore_entry kv_entry; typedef ds4_kvstore_options kv_cache_options; typedef ds4_kvstore kv_disk_cache; typedef enum { TOOL_MEMORY_RAM = 0, TOOL_MEMORY_DISK = 1, } tool_memory_source; typedef struct tool_memory_entry tool_memory_entry; typedef struct { char *dsml; size_t len; size_t bytes; int refs; uint64_t seen; tool_memory_entry *entries; } tool_memory_block; struct tool_memory_entry { char *id; tool_memory_block *block; size_t bytes; uint64_t stamp; tool_memory_source source; tool_memory_entry *prev; tool_memory_entry *next; tool_memory_entry *block_next; }; typedef struct { rax *by_id; rax *by_block; tool_memory_entry *head; tool_memory_entry *tail; int entries; int max_entries; size_t bytes; size_t max_bytes; uint64_t clock; uint64_t scan_clock; } tool_memory; /* Image markers have request-local nonces. Normalize only actual marker spans * for visible-history matching, and retain their byte offsets so literal text * cannot impersonate an image. Full fingerprints are checked against live KV * before any prefix is reused. Normalization preserves all byte offsets. */ typedef struct { size_t count; size_t offsets[16]; } visible_image_key; static char *visible_prompt_key(const request *req, const char *text, visible_image_key *images) { memset(images, 0, sizeof(*images)); if (!req || !text || req->image_count > 16 || (req->image_count && !req->image_markers)) return NULL; char *key = xstrdup(text); const char *cursor = text; for (size_t i = 0; i < req->image_count; i++) { const char *marker = strstr(cursor, req->image_markers[i]); size_t len = strlen(req->image_markers[i]); if (!marker || !len) { free(key); return NULL; } images->offsets[images->count++] = (size_t)(marker - text); memset(key + (marker - text), '\036', len); cursor = marker + len; } return key; } static bool visible_image_prefix_matches(const visible_image_key *incoming, const visible_image_key *old, size_t boundary) { if (incoming->count < old->count) return false; for (size_t i = 0; i < old->count; i++) if (incoming->offsets[i] != old->offsets[i]) return false; return incoming->count == old->count || incoming->offsets[old->count] >= boundary; } typedef struct { bool valid; /* Token frontier of a live assistant tool-call turn. Continuing from this * point preserves hidden thinking and sampled DSML bytes that are not * necessarily present in the client-visible replay. */ int live_tokens; /* Optional rendered conversation text that the client is expected to replay. * Responses uses this because visible replay can omit hidden reasoning. * Anthropic currently uses only the call-id side of the state. */ char *visible_text; size_t visible_len; visible_image_key images; /* Tool-call ids generated at the same live frontier. A following tool * result for these ids is a direct protocol continuation and should not * trigger prompt-prefix matching or checkpoint canonicalization. */ stop_list call_ids; } live_tool_state; typedef struct { bool valid; /* Token frontier of the live sampled session. The visible text below is * what clients will replay, but the payload at this frontier may also * contain hidden thinking tokens that are intentionally absent from that * visible replay. */ int live_tokens; char *visible_text; size_t visible_len; visible_image_key images; /* Keep exact disk replay for clients that preserve reasoning. Switch to * the visible key once a request actually continues without reasoning. */ bool token_text_disk_key; } visible_live_state; struct server_slot { server *srv; int id; ds4_session *session; live_tool_state responses_live; live_tool_state anthropic_live; visible_live_state thinking_live; int continued_last_store_tokens; /* Wall time of the last completed job on this slot, stamped by the slot * worker; drives the staleness tiers in job_slot_score(). */ time_t last_used; /* Cached rendered text of the checkpoint, refreshed by the slot worker * after each job; lets the reuse probe (slot_probe_reuse_locked) answer * the memory-text question with a memcmp instead of a detokenization. */ char *live_text; size_t live_text_len; int live_text_pos; /* checkpoint.len at render time; 0 = stale */ job *assigned; job *running; bool busy; bool prefill_waiting; bool decode_pending; bool decode_in_flight; bool decode_done; int decode_token; bool decode_speculative; int decode_accepted[2]; int decode_accepted_count; int decode_rc; char decode_err[160]; }; #define SERVER_IMAGE_CACHE_ENTRIES 32 #define SERVER_IMAGE_CACHE_BYTES (128u * 1024u * 1024u) typedef struct { uint8_t *encoded; size_t encoded_len; ds4_vision_embedding embedding; size_t data_bytes; uint64_t used; } server_image_cache_entry; typedef struct { server_image_cache_entry entries[SERVER_IMAGE_CACHE_ENTRIES]; size_t bytes; uint64_t clock; } server_image_cache; static void server_image_cache_remove(server_image_cache *cache, server_image_cache_entry *entry) { cache->bytes -= entry->encoded_len + entry->data_bytes; free(entry->encoded); ds4_vision_embedding_free(&entry->embedding); memset(entry, 0, sizeof(*entry)); } static void server_image_cache_clear(server_image_cache *cache) { for (size_t i = 0; i < SERVER_IMAGE_CACHE_ENTRIES; i++) server_image_cache_remove(cache, &cache->entries[i]); cache->clock = 0; } static bool server_image_cache_get(server_image_cache *cache, const server_image_input *input, ds4_vision_embedding *out) { for (size_t i = 0; i < SERVER_IMAGE_CACHE_ENTRIES; i++) { server_image_cache_entry *entry = &cache->entries[i]; if (!entry->encoded || entry->encoded_len != input->encoded_len || memcmp(entry->encoded, input->encoded, input->encoded_len)) continue; float *data = malloc(entry->data_bytes); if (!data) return false; memcpy(data, entry->embedding.data, entry->data_bytes); *out = entry->embedding; out->data = data; entry->used = ++cache->clock; return true; } return false; } /* Cache only successful encodes. Exact byte keys avoid trusting a client hash; * callers own a copy, since DeepSeek expands the natural embedding in place. */ static void server_image_cache_put(server_image_cache *cache, const server_image_input *input, const ds4_vision_embedding *embedding, uint32_t dim, size_t budget) { if (!embedding->data || !dim || !embedding->token_count || !input->encoded_len || embedding->token_count > budget / sizeof(float) / dim) return; size_t bytes = (size_t)embedding->token_count * dim * sizeof(float); if (input->encoded_len > budget - bytes) return; size_t total = input->encoded_len + bytes; server_image_cache_entry *dest; for (;;) { dest = &cache->entries[0]; for (size_t i = 1; i < SERVER_IMAGE_CACHE_ENTRIES; i++) { if (cache->entries[i].used < dest->used) dest = &cache->entries[i]; } if (!dest->encoded && cache->bytes <= budget - total) break; if (!dest->encoded) { for (size_t i = 0; i < SERVER_IMAGE_CACHE_ENTRIES; i++) { server_image_cache_entry *entry = &cache->entries[i]; if (entry->encoded && (!dest->encoded || entry->used < dest->used)) dest = entry; } } server_image_cache_remove(cache, dest); } uint8_t *encoded = malloc(input->encoded_len); float *data = malloc((size_t)bytes); if (!encoded || !data) { free(encoded); free(data); return; } memcpy(encoded, input->encoded, input->encoded_len); memcpy(data, embedding->data, (size_t)bytes); *dest = (server_image_cache_entry) { .encoded = encoded, .encoded_len = input->encoded_len, .embedding = *embedding, .data_bytes = (size_t)bytes, .used = ++cache->clock, }; dest->embedding.data = data; cache->bytes += total; } static bool id_list_contains(const stop_list *ids, const char *id); static void id_list_push_unique(stop_list *ids, const char *id); struct server { ds4_engine *engine; ds4_tp *tp_leader; server_slot *slots; int slot_count; int ctx_size; bool batched_mode; pthread_t *slot_threads; pthread_t decode_thread; int default_tokens; kv_disk_cache kv; tool_memory tool_mem; server_image_cache image_cache; /* Protected by inference_mu. */ bool disable_exact_dsml_tool_replay; bool enable_cors; pthread_mutex_t tool_mu; pthread_mutex_t kv_mu; pthread_mutex_t inference_mu; pthread_mutex_t model_mu; pthread_cond_t model_cv; bool model_busy; bool qwen4_batch_mtp; bool model_stopping; int decode_pending; int active_generations; int mixed_prefill_quantum; int idle_prefill_quantum; /* the engine's prefill cap: wide chunks while nothing generates */ int last_prefill_slot; pthread_mutex_t mu; pthread_cond_t cv; pthread_cond_t clients_cv; job *head; job *tail; bool stopping; int clients; FILE *trace; pthread_mutex_t trace_mu; uint64_t trace_seq; }; static void server_inference_lock(server *s) { pthread_mutex_lock(&s->inference_mu); } static void server_inference_unlock(server *s) { pthread_mutex_unlock(&s->inference_mu); } /* The caller holds inference_mu across cache lookup and encoder execution. */ static bool server_encode_image(server *s, const server_image_input *input, ds4_vision_embedding *out, char *err, size_t errlen) { if (server_image_cache_get(&s->image_cache, input, out)) { server_log(DS4_LOG_KVCACHE, "ds4-server: vision embedding cache hit"); return true; } if (!ds4_engine_vision_encode_memory(s->engine, input->encoded, input->encoded_len, out, err, errlen)) return false; /* Row width must match the encoder's allocation: the mmproj projection * dim is validated to equal the model's n_embd at load (Qwen3.8 is * 2560-wide, not 4096; a wrong width over-reads the source in put). */ server_image_cache_put(&s->image_cache, input, out, (uint32_t)ds4_engine_embd_dim(s->engine), SERVER_IMAGE_CACHE_BYTES); server_log(DS4_LOG_KVCACHE, "ds4-server: vision embedding encoded; cache=%zu bytes", s->image_cache.bytes); return true; } /* Jobs are stack-owned by the client thread. A resident-slot worker signals * completion after it has written the response, so request data and the socket * remain valid without heap-allocating per-request job objects. */ struct job { int fd; request req; bool done; bool cancelled; pthread_mutex_t mu; pthread_cond_t cv; job *next; }; static bool job_cancelled(void *ud) { job *j = ud; if (!j) return false; pthread_mutex_lock(&j->mu); bool cancelled = j->cancelled; pthread_mutex_unlock(&j->mu); return cancelled; } static void job_mark_cancelled(job *j) { if (!j) return; pthread_mutex_lock(&j->mu); j->cancelled = true; pthread_mutex_unlock(&j->mu); } static void job_complete(job *j) { pthread_mutex_lock(&j->mu); j->done = true; pthread_cond_signal(&j->cv); pthread_mutex_unlock(&j->mu); } static bool slot_job_cancelled(const server_slot *slot) { return slot && slot->running && job_cancelled(slot->running); } /* ========================================================================= * Tool Call Text Memory. * ========================================================================= * * The model speaks DSML, while OpenAI and Anthropic clients round-trip tool * calls as JSON. Re-rendering that JSON is not always the same byte sequence: * clients may preserve, sort, or rebuild object keys differently. Tool call * ids are the bridge between both worlds. For every generated tool call we * remember the exact DSML block sampled by the model under a random id. When * the client later sends the same id back in conversation history, we replay * the sampled DSML verbatim and keep the KV cache aligned with the live model * state. */ #define DS4_TOOL_MEMORY_DEFAULT_MAX_IDS 100000 #define DS4_TOOL_MEMORY_MAX_BYTES (512u * 1024u * 1024u) static int tool_memory_max_entries(const tool_memory *m) { return m && m->max_entries > 0 ? m->max_entries : DS4_TOOL_MEMORY_DEFAULT_MAX_IDS; } static size_t tool_memory_max_bytes(const tool_memory *m) { return m && m->max_bytes > 0 ? m->max_bytes : DS4_TOOL_MEMORY_MAX_BYTES; } static void tool_memory_init_locked(tool_memory *m) { if (m->by_id && m->by_block) return; m->by_id = raxNew(); m->by_block = raxNew(); if (!m->by_id || !m->by_block) die("out of memory"); } static void tool_memory_link_head(tool_memory *m, tool_memory_entry *e) { e->prev = NULL; e->next = m->head; if (m->head) m->head->prev = e; else m->tail = e; m->head = e; } static void tool_memory_unlink(tool_memory *m, tool_memory_entry *e) { if (e->prev) e->prev->next = e->next; else m->head = e->next; if (e->next) e->next->prev = e->prev; else m->tail = e->prev; e->prev = e->next = NULL; } static void tool_memory_touch(tool_memory *m, tool_memory_entry *e) { e->stamp = ++m->clock; if (m->head == e) return; tool_memory_unlink(m, e); tool_memory_link_head(m, e); } static void tool_block_unlink_entry(tool_memory_block *b, tool_memory_entry *e) { tool_memory_entry **p = &b->entries; while (*p) { if (*p == e) { *p = e->block_next; e->block_next = NULL; return; } p = &(*p)->block_next; } } static tool_memory_block *tool_memory_find_block_locked(tool_memory *m, const char *dsml, size_t len) { if (!m->by_block || !dsml || len == 0) return NULL; void *v = raxFind(m->by_block, (unsigned char *)dsml, len); return v == raxNotFound ? NULL : v; } static tool_memory_block *tool_memory_get_block_locked(tool_memory *m, const char *dsml, size_t len) { tool_memory_block *b = tool_memory_find_block_locked(m, dsml, len); if (b) return b; b = xmalloc(sizeof(*b)); memset(b, 0, sizeof(*b)); b->dsml = xstrndup(dsml, len); b->len = len; b->bytes = len + 1 + sizeof(*b); if (!raxInsert(m->by_block, (unsigned char *)b->dsml, b->len, b, NULL)) { free(b->dsml); free(b); die("out of memory"); } m->bytes += b->bytes; return b; } static void tool_memory_release_block_locked(tool_memory *m, tool_memory_block *b) { if (!b) return; if (--b->refs > 0) return; if (m->by_block) { void *old = NULL; (void)raxRemove(m->by_block, (unsigned char *)b->dsml, b->len, &old); } if (m->bytes >= b->bytes) m->bytes -= b->bytes; else m->bytes = 0; free(b->dsml); free(b); } static void tool_memory_remove_entry_locked(tool_memory *m, tool_memory_entry *e) { if (!e) return; if (m->by_id && e->id) { void *old = NULL; (void)raxRemove(m->by_id, (unsigned char *)e->id, strlen(e->id), &old); } tool_memory_unlink(m, e); if (e->block) tool_block_unlink_entry(e->block, e); if (m->bytes >= e->bytes) m->bytes -= e->bytes; else m->bytes = 0; if (m->entries > 0) m->entries--; free(e->id); tool_memory_release_block_locked(m, e->block); free(e); } static void tool_memory_prune_locked(tool_memory *m) { while ((m->entries > tool_memory_max_entries(m) || m->bytes > tool_memory_max_bytes(m)) && m->tail) { tool_memory_remove_entry_locked(m, m->tail); } } static tool_memory_entry *tool_memory_find_entry_locked(tool_memory *m, const char *id) { if (!m->by_id || !id || !id[0]) return NULL; void *v = raxFind(m->by_id, (unsigned char *)id, strlen(id)); return v == raxNotFound ? NULL : v; } static void tool_memory_put_locked(tool_memory *m, const char *id, const char *dsml, tool_memory_source source) { if (!id || !id[0] || !dsml || !dsml[0]) return; tool_memory_init_locked(m); size_t dsml_len = strlen(dsml); tool_memory_entry *old = tool_memory_find_entry_locked(m, id); if (old && old->block && old->block->len == dsml_len && !memcmp(old->block->dsml, dsml, dsml_len)) { if (source == TOOL_MEMORY_RAM) old->source = TOOL_MEMORY_RAM; tool_memory_touch(m, old); tool_memory_prune_locked(m); return; } if (old) tool_memory_remove_entry_locked(m, old); tool_memory_block *b = tool_memory_get_block_locked(m, dsml, dsml_len); tool_memory_entry *e = xmalloc(sizeof(*e)); memset(e, 0, sizeof(*e)); e->id = xstrdup(id); e->block = b; e->bytes = strlen(id) + 1 + sizeof(*e); e->stamp = ++m->clock; e->source = source; e->block_next = b->entries; b->entries = e; b->refs++; if (!raxInsert(m->by_id, (unsigned char *)e->id, strlen(e->id), e, NULL)) { tool_block_unlink_entry(b, e); free(e->id); free(e); tool_memory_release_block_locked(m, b); die("out of memory"); } tool_memory_link_head(m, e); m->entries++; m->bytes += e->bytes; tool_memory_prune_locked(m); } static void tool_memory_free(tool_memory *m) { while (m->tail) tool_memory_remove_entry_locked(m, m->tail); if (m->by_id) raxFree(m->by_id); if (m->by_block) raxFree(m->by_block); memset(m, 0, sizeof(*m)); } /* Single live protocol-tool state. * * This is not an implementation of durable remote conversation storage. It is * only an in-memory binding from protocol tool-call IDs to the current sampled * KV frontier. If it does not match, DS4 falls back to the same prefix and * disk-cache machinery used by chat/completions, or returns a clear error for * tool-result-only requests that have no replayable prefix. */ static void live_tool_state_clear_locked(live_tool_state *st) { if (!st) return; stop_list_clear(&st->call_ids); free(st->visible_text); st->visible_text = NULL; st->visible_len = 0; memset(&st->images, 0, sizeof(st->images)); st->valid = false; st->live_tokens = 0; } static void live_tool_state_free(live_tool_state *st) { if (!st) return; live_tool_state_clear_locked(st); free(st->call_ids.v); memset(st, 0, sizeof(*st)); } static void visible_live_clear_locked(visible_live_state *st) { if (!st) return; free(st->visible_text); st->visible_text = NULL; st->visible_len = 0; memset(&st->images, 0, sizeof(st->images)); st->live_tokens = 0; st->valid = false; st->token_text_disk_key = false; } static void visible_live_free(visible_live_state *st) { if (!st) return; visible_live_clear_locked(st); memset(st, 0, sizeof(*st)); } static void thinking_live_clear(server *s, server_slot *slot) { if (!s || !slot) return; pthread_mutex_lock(&s->tool_mu); visible_live_clear_locked(&slot->thinking_live); pthread_mutex_unlock(&s->tool_mu); } static void thinking_live_remember(server *s, server_slot *slot, const char *visible_text, const request *req) { if (!s || !slot || !visible_text || !visible_text[0]) return; visible_image_key images; char *key = visible_prompt_key(req, visible_text, &images); pthread_mutex_lock(&s->tool_mu); visible_live_clear_locked(&slot->thinking_live); slot->thinking_live.visible_text = key; slot->thinking_live.visible_len = key ? strlen(key) : 0; slot->thinking_live.images = images; slot->thinking_live.live_tokens = ds4_session_pos(slot->session); slot->thinking_live.valid = key != NULL; pthread_mutex_unlock(&s->tool_mu); } static void responses_live_remember(server *s, server_slot *slot, const char *visible_text, const tool_calls *calls, const request *req) { if (!s || !slot || !visible_text || !visible_text[0]) return; visible_image_key images; char *key = visible_prompt_key(req, visible_text, &images); pthread_mutex_lock(&s->tool_mu); live_tool_state_clear_locked(&slot->responses_live); slot->responses_live.visible_text = key; slot->responses_live.visible_len = key ? strlen(key) : 0; slot->responses_live.images = images; if (calls) { for (int i = 0; i < calls->len; i++) { id_list_push_unique(&slot->responses_live.call_ids, calls->v[i].id); } } slot->responses_live.live_tokens = ds4_session_pos(slot->session); slot->responses_live.valid = key != NULL; pthread_mutex_unlock(&s->tool_mu); } static void anthropic_live_remember(server *s, server_slot *slot, const tool_calls *calls) { if (!s || !slot || !calls || calls->len == 0) return; pthread_mutex_lock(&s->tool_mu); live_tool_state_clear_locked(&slot->anthropic_live); for (int i = 0; i < calls->len; i++) { id_list_push_unique(&slot->anthropic_live.call_ids, calls->v[i].id); } slot->anthropic_live.live_tokens = ds4_session_pos(slot->session); slot->anthropic_live.valid = slot->anthropic_live.call_ids.len > 0; pthread_mutex_unlock(&s->tool_mu); } static void responses_live_clear(server *s, server_slot *slot) { if (!s || !slot) return; pthread_mutex_lock(&s->tool_mu); live_tool_state_clear_locked(&slot->responses_live); pthread_mutex_unlock(&s->tool_mu); } static void anthropic_live_clear(server *s, server_slot *slot) { if (!s || !slot) return; pthread_mutex_lock(&s->tool_mu); live_tool_state_clear_locked(&slot->anthropic_live); pthread_mutex_unlock(&s->tool_mu); } static void request_live_state_clear(server *s, server_slot *slot) { responses_live_clear(s, slot); anthropic_live_clear(s, slot); thinking_live_clear(s, slot); } static bool responses_live_has_call_id(server *s, const char *id) { if (!s || !id || !id[0]) return false; pthread_mutex_lock(&s->tool_mu); bool found = false; for (int i = 0; i < s->slot_count && !found; i++) { found = s->slots[i].responses_live.valid && id_list_contains(&s->slots[i].responses_live.call_ids, id); } pthread_mutex_unlock(&s->tool_mu); return found; } static bool anthropic_live_has_call_id(server *s, const char *id) { if (!s || !id || !id[0]) return false; pthread_mutex_lock(&s->tool_mu); bool found = false; for (int i = 0; i < s->slot_count && !found; i++) { found = s->slots[i].anthropic_live.valid && id_list_contains(&s->slots[i].anthropic_live.call_ids, id); } pthread_mutex_unlock(&s->tool_mu); return found; } static stop_list live_tool_call_order(server *s, api_style api, const stop_list *ids) { stop_list order = {0}; if (!s || !ids || !ids->len || (api != API_RESPONSES && api != API_ANTHROPIC)) return order; pthread_mutex_lock(&s->tool_mu); for (int i = 0; i < s->slot_count; i++) { const live_tool_state *live = api == API_RESPONSES ? &s->slots[i].responses_live : &s->slots[i].anthropic_live; if (!live->valid || live->call_ids.len != ids->len) continue; bool match = true; for (int j = 0; j < ids->len && match; j++) match = id_list_contains(&live->call_ids, ids->v[j]); if (!match) continue; for (int j = 0; j < live->call_ids.len; j++) id_list_push_unique(&order, live->call_ids.v[j]); break; } pthread_mutex_unlock(&s->tool_mu); return order; } static bool responses_live_matches_request_locked(server_slot *slot, const stop_list *ids, int live_tokens) { bool ok = slot->responses_live.valid && slot->responses_live.live_tokens == live_tokens && slot->responses_live.call_ids.len == ids->len; for (int i = 0; ok && i < ids->len; i++) { ok = id_list_contains(&slot->responses_live.call_ids, ids->v[i]); } return ok; } static bool responses_live_matches_request(server *s, server_slot *slot, const stop_list *ids, int live_tokens) { if (!s || !slot || !ids || ids->len == 0) return false; pthread_mutex_lock(&s->tool_mu); bool ok = responses_live_matches_request_locked(slot, ids, live_tokens); pthread_mutex_unlock(&s->tool_mu); return ok; } static bool anthropic_live_matches_request_locked(server_slot *slot, const stop_list *ids, int live_tokens) { bool ok = slot->anthropic_live.valid && slot->anthropic_live.live_tokens == live_tokens && slot->anthropic_live.call_ids.len == ids->len; for (int i = 0; ok && i < ids->len; i++) { ok = id_list_contains(&slot->anthropic_live.call_ids, ids->v[i]); } return ok; } static bool tool_memory_has_id(server *s, const char *id) { if (!s || s->disable_exact_dsml_tool_replay || !id || !id[0]) return false; pthread_mutex_lock(&s->tool_mu); bool found = tool_memory_find_entry_locked(&s->tool_mem, id) != NULL; pthread_mutex_unlock(&s->tool_mu); return found; } static const char *tool_memory_lookup_locked(tool_memory *m, const char *id, tool_memory_source *source, tool_memory_block **block) { tool_memory_entry *e = tool_memory_find_entry_locked(m, id); if (!e || !e->block) return NULL; tool_memory_touch(m, e); if (source) *source = e->source; if (block) *block = e->block; return e->block->dsml; } static void tool_memory_remember(server *s, const tool_calls *calls) { if (!s || s->disable_exact_dsml_tool_replay || !calls || !calls->raw_tool_text || !calls->raw_tool_text[0]) return; pthread_mutex_lock(&s->tool_mu); for (int i = 0; i < calls->len; i++) { tool_memory_put_locked(&s->tool_mem, calls->v[i].id, calls->raw_tool_text, TOOL_MEMORY_RAM); } pthread_mutex_unlock(&s->tool_mu); } static void tool_memory_put_source(server *s, const char *id, const char *dsml, tool_memory_source source) { if (!s || s->disable_exact_dsml_tool_replay || !id || !id[0] || !dsml || !dsml[0]) return; pthread_mutex_lock(&s->tool_mu); tool_memory_put_locked(&s->tool_mem, id, dsml, source); pthread_mutex_unlock(&s->tool_mu); } #ifdef DS4_SERVER_TEST static void tool_memory_put(server *s, const char *id, const char *dsml) { tool_memory_put_source(s, id, dsml, TOOL_MEMORY_RAM); } #endif static void tool_memory_attach_to_messages(server *s, chat_msgs *msgs, tool_replay_stats *stats) { if (!msgs) return; if (!s || s->disable_exact_dsml_tool_replay) { if (stats) { for (int i = 0; i < msgs->len; i++) { tool_calls *calls = &msgs->v[i].calls; if (calls->len == 0 || calls->raw_tool_text) continue; stats->canonical++; stats->missing_ids += calls->len; } } return; } pthread_mutex_lock(&s->tool_mu); for (int i = 0; i < msgs->len; i++) { tool_calls *calls = &msgs->v[i].calls; if (calls->len == 0 || calls->raw_tool_text) continue; tool_memory_block *matched = NULL; tool_memory_source matched_source = TOOL_MEMORY_DISK; bool exact = true; int missing = 0; for (int j = 0; j < calls->len; j++) { tool_memory_source source = TOOL_MEMORY_DISK; tool_memory_block *block = NULL; const char *dsml = tool_memory_lookup_locked(&s->tool_mem, calls->v[j].id, &source, &block); if (!dsml) { exact = false; missing++; continue; } if (!matched) { matched = block; matched_source = source; } else if (matched != block) { exact = false; } if (source == TOOL_MEMORY_RAM) matched_source = TOOL_MEMORY_RAM; } if (exact && matched) { calls->raw_tool_text = xstrdup(matched->dsml); if (stats) { if (matched_source == TOOL_MEMORY_RAM) stats->mem++; else stats->disk++; } } else if (stats) { stats->canonical++; stats->missing_ids += missing; } } pthread_mutex_unlock(&s->tool_mu); } static bool tool_calls_contains_id(const tool_calls *calls, const char *id, int upto) { if (!calls || !id || !id[0]) return false; if (upto > calls->len) upto = calls->len; for (int i = 0; i < upto; i++) { if (calls->v[i].id && !strcmp(calls->v[i].id, id)) return true; } return false; } static void assign_tool_call_ids(server *s, tool_calls *calls, api_style api) { if (!calls) return; for (int i = 0; i < calls->len; i++) { if (calls->v[i].id && calls->v[i].id[0]) continue; char id[64]; for (;;) { random_tool_id(id, sizeof(id), api); if (!tool_calls_contains_id(calls, id, i) && !tool_memory_has_id(s, id)) break; } calls->v[i].id = xstrdup(id); } } static void apply_openai_stream_tool_ids(tool_calls *calls, const openai_stream *st) { if (!calls || !st) return; int n = calls->len < st->tool.ids_cap ? calls->len : st->tool.ids_cap; for (int i = 0; i < n; i++) { if (calls->v[i].id && calls->v[i].id[0]) continue; if (st->tool.ids[i] && st->tool.ids[i][0]) calls->v[i].id = xstrdup(st->tool.ids[i]); } } static void apply_anthropic_stream_tool_ids(tool_calls *calls, const anthropic_stream *st) { if (!calls || !st) return; /* The SSE stream may have exposed tool ids before final DSML parsing. The * parsed calls must inherit those ids before assign_tool_call_ids() and * tool_memory_remember(), otherwise the client returns a tool_result for an * id that the continuation fast path does not know. */ int n = calls->len < st->tool.ids_cap ? calls->len : st->tool.ids_cap; for (int i = 0; i < n; i++) { if (calls->v[i].id && calls->v[i].id[0]) continue; if (st->tool.ids[i] && st->tool.ids[i][0]) calls->v[i].id = xstrdup(st->tool.ids[i]); } } /* ========================================================================= * KV Cache. * ========================================================================= * * The server has one live Metal session. We persist reusable DS4 session * snapshots when a cold prompt reaches a useful prefix, when a long continued * conversation has grown far enough, and when a request evicts the live session. * The cache key is the SHA1 of the rendered byte prefix. The payload still * stores exact token IDs and graph state; the filename only selects a checkpoint * whose decoded transcript bytes are a prefix of the next rendered request. * * Files are loaded with plain read/write I/O into the existing graph tensors; * mmap is deliberately avoided here so cache restore cannot add more VM * mappings to a process that already maps a very large GGUF. * * Stores are created only when the live graph is already at the checkpoint we * want to persist. For long cold prompts this means prefill reaches the stable * boundary first, writes that prefix, and then continues with the suffix. We * never roll the session backward just to build a disk cache entry: that would * turn cache population into a second hidden prefill. * * File layout: * * "KVC" version * quant bits, save reason, token count, hit count, context size * creation time, last-used time, payload byte count * rendered text byte count + rendered text for human inspection * DS4 engine payload written by ds4_session_save_payload() * optional tool-id map section * * The filename is SHA1(cache text bytes), not SHA1(token ids). For ordinary * checkpoints the cache text is the rendered token prefix. For live hidden * state it can instead be the client-visible transcript: the payload still * contains sampled reasoning KV, but the lookup key must be what the client can * replay after a process restart or session switch. * * The optional tool-id map is not part of model state, but it is needed to * render future client JSON back to the exact DSML sampled by the model. We * persist only mappings whose DSML block appears in the saved cache text. */ #define KV_CACHE_FIXED_HEADER DS4_KVSTORE_FIXED_HEADER #define KV_CACHE_HIT_HALF_LIFE_SECONDS DS4_KVSTORE_HIT_HALF_LIFE_SECONDS #define KV_EXT_TOOL_MAP DS4_KVSTORE_EXT_TOOL_MAP #define KV_EXT_RESPONSES_VISIBLE DS4_KVSTORE_EXT_RESPONSES_VISIBLE #define KV_EXT_THINKING_VISIBLE DS4_KVSTORE_EXT_THINKING_VISIBLE #define KV_TOOL_MAP_MAGIC0 'K' #define KV_TOOL_MAP_MAGIC1 'T' #define KV_TOOL_MAP_MAGIC2 'M' #define KV_TOOL_MAP_VERSION 1u #define KV_TOOL_MAP_HEADER 8u typedef enum { KV_REASON_UNKNOWN = DS4_KVSTORE_REASON_UNKNOWN, KV_REASON_COLD = DS4_KVSTORE_REASON_COLD, KV_REASON_CONTINUED = DS4_KVSTORE_REASON_CONTINUED, KV_REASON_EVICT = DS4_KVSTORE_REASON_EVICT, KV_REASON_SHUTDOWN = DS4_KVSTORE_REASON_SHUTDOWN, } kv_cache_reason; static kv_cache_options kv_cache_default_options(void) { return ds4_kvstore_default_options(); } static void le_put32(uint8_t *p, uint32_t v) { ds4_kvstore_le_put32(p, v); } static uint32_t le_get32(const uint8_t *p) { return ds4_kvstore_le_get32(p); } #ifdef DS4_SERVER_TEST static void sha1_bytes_hex(const void *ptr, size_t len, char out[41]) { ds4_kvstore_sha1_bytes_hex(ptr, len, out); } #endif static bool id_list_contains(const stop_list *ids, const char *id) { if (!ids || !id || !id[0]) return false; for (int i = 0; i < ids->len; i++) { if (ids->v[i] && !strcmp(ids->v[i], id)) return true; } return false; } static void id_list_push_unique(stop_list *ids, const char *id) { if (!ids || !id || !id[0] || id_list_contains(ids, id)) return; stop_list_push(ids, xstrdup(id)); } static void id_list_free(stop_list *ids) { stop_list_clear(ids); free(ids->v); memset(ids, 0, sizeof(*ids)); } static void collect_tool_call_ids(const chat_msgs *msgs, stop_list *ids) { if (!msgs || !ids) return; for (int i = 0; i < msgs->len; i++) { id_list_push_unique(ids, msgs->v[i].tool_call_id); for (int j = 0; j < msgs->v[i].tool_call_ids_len; j++) { id_list_push_unique(ids, msgs->v[i].tool_call_ids[j]); } const tool_calls *calls = &msgs->v[i].calls; for (int j = 0; j < calls->len; j++) { id_list_push_unique(ids, calls->v[j].id); } } } static bool sha_hex_name(const char *name, char sha[41]) { return ds4_kvstore_sha_hex_name(name, sha); } static char *path_join(const char *dir, const char *name) { return ds4_kvstore_path_join(dir, name); } static const char *find_next_dsml_tool_block(const char *p, const char **end_out) { struct block_form { const char *start; const char *end; } forms[] = { {"\n\n" DS41_TOOL_CALLS_START, DS41_TOOL_CALLS_END}, {DS41_TOOL_CALLS_START, DS41_TOOL_CALLS_END}, {"\n\n" DS4_TOOL_CALLS_START, DS4_TOOL_CALLS_END}, {DS4_TOOL_CALLS_START, DS4_TOOL_CALLS_END}, {"\n\n" DS4_TOOL_CALLS_START_SHORT, DS4_TOOL_CALLS_END_SHORT}, {DS4_TOOL_CALLS_START_SHORT, DS4_TOOL_CALLS_END_SHORT}, {"\n\n", ""}, {"", ""}, }; const char *best = NULL; const char *best_end = NULL; for (size_t i = 0; i < sizeof(forms) / sizeof(forms[0]); i++) { const char *s = strstr(p, forms[i].start); if (!s || (best && s >= best)) continue; const char *e = strstr(s, forms[i].end); if (!e) continue; best = s; best_end = e + strlen(forms[i].end); } if (end_out) *end_out = best_end; return best; } static bool kv_tool_map_measure_locked(server *s, const char *text, uint32_t *count_out, uint64_t *bytes_out) { uint32_t count = 0; uint64_t bytes = KV_TOOL_MAP_HEADER; uint64_t scan = ++s->tool_mem.scan_clock; const char *p = text; for (;;) { const char *end = NULL; const char *start = find_next_dsml_tool_block(p, &end); if (!start || !end) break; tool_memory_block *b = tool_memory_find_block_locked(&s->tool_mem, start, (size_t)(end - start)); if (b && b->seen != scan) { b->seen = scan; for (tool_memory_entry *e = b->entries; e; e = e->block_next) { size_t id_len = strlen(e->id); size_t dsml_len = b->len; if (id_len > UINT32_MAX || dsml_len > UINT32_MAX) continue; if (count == UINT32_MAX) return false; if (UINT64_MAX - bytes < 8u || UINT64_MAX - bytes - 8u < (uint64_t)id_len || UINT64_MAX - bytes - 8u - (uint64_t)id_len < (uint64_t)dsml_len) return false; count++; bytes += 8u + (uint64_t)id_len + (uint64_t)dsml_len; } } p = end; } if (count == 0) bytes = 0; if (count_out) *count_out = count; if (bytes_out) *bytes_out = bytes; return true; } static bool kv_tool_map_serialized_size(server *s, const char *text, uint64_t *bytes_out) { if (bytes_out) *bytes_out = 0; if (!s || s->disable_exact_dsml_tool_replay || !text || !text[0]) return true; pthread_mutex_lock(&s->tool_mu); bool ok = kv_tool_map_measure_locked(s, text, NULL, bytes_out); pthread_mutex_unlock(&s->tool_mu); return ok; } static bool kv_tool_map_write(server *s, FILE *fp, const char *text, uint64_t *written_bytes) { if (written_bytes) *written_bytes = 0; if (!s || s->disable_exact_dsml_tool_replay || !fp || !text || !text[0]) return true; pthread_mutex_lock(&s->tool_mu); uint32_t count = 0; uint64_t bytes = 0; bool ok = kv_tool_map_measure_locked(s, text, &count, &bytes); if (!ok) { pthread_mutex_unlock(&s->tool_mu); return false; } if (count == 0) { pthread_mutex_unlock(&s->tool_mu); return true; } uint8_t h[KV_TOOL_MAP_HEADER]; h[0] = KV_TOOL_MAP_MAGIC0; h[1] = KV_TOOL_MAP_MAGIC1; h[2] = KV_TOOL_MAP_MAGIC2; h[3] = KV_TOOL_MAP_VERSION; le_put32(h + 4, count); ok = fwrite(h, 1, sizeof(h), fp) == sizeof(h); uint64_t scan = ++s->tool_mem.scan_clock; const char *p = text; for (;;) { const char *end = NULL; const char *start = find_next_dsml_tool_block(p, &end); if (!start || !end || !ok) break; tool_memory_block *b = tool_memory_find_block_locked(&s->tool_mem, start, (size_t)(end - start)); if (b && b->seen != scan) { b->seen = scan; for (tool_memory_entry *e = b->entries; ok && e; e = e->block_next) { size_t id_len = strlen(e->id); size_t dsml_len = b->len; if (id_len > UINT32_MAX || dsml_len > UINT32_MAX) continue; uint8_t lens[8]; le_put32(lens, (uint32_t)id_len); le_put32(lens + 4, (uint32_t)dsml_len); ok = fwrite(lens, 1, sizeof(lens), fp) == sizeof(lens) && fwrite(e->id, 1, id_len, fp) == id_len && fwrite(b->dsml, 1, dsml_len, fp) == dsml_len; } } p = end; } pthread_mutex_unlock(&s->tool_mu); if (ok && written_bytes) *written_bytes = bytes; return ok; } static int kv_tool_map_load_from_pos(server *s, FILE *fp, const stop_list *wanted) { if (!s || s->disable_exact_dsml_tool_replay || !fp) return 0; uint8_t h[KV_TOOL_MAP_HEADER]; size_t n = fread(h, 1, sizeof(h), fp); if (n == 0 && feof(fp)) return 0; if (n != sizeof(h)) return 0; if (h[0] != KV_TOOL_MAP_MAGIC0 || h[1] != KV_TOOL_MAP_MAGIC1 || h[2] != KV_TOOL_MAP_MAGIC2 || h[3] != KV_TOOL_MAP_VERSION) return 0; uint32_t count = le_get32(h + 4); if ((uint64_t)count > (uint64_t)tool_memory_max_entries(&s->tool_mem) * 4u) return 0; int loaded = 0; for (uint32_t i = 0; i < count; i++) { uint8_t lens[8]; if (fread(lens, 1, sizeof(lens), fp) != sizeof(lens)) return loaded; uint32_t id_len = le_get32(lens); uint32_t dsml_len = le_get32(lens + 4); if (id_len == 0 || id_len > 256 || dsml_len == 0 || dsml_len > DS4_TOOL_MEMORY_MAX_BYTES) return loaded; char *id = xmalloc((size_t)id_len + 1); char *dsml = xmalloc((size_t)dsml_len + 1); bool ok = fread(id, 1, id_len, fp) == id_len && fread(dsml, 1, dsml_len, fp) == dsml_len; id[id_len] = '\0'; dsml[dsml_len] = '\0'; if (ok && (!wanted || id_list_contains(wanted, id))) { tool_memory_put_source(s, id, dsml, TOOL_MEMORY_DISK); loaded++; } free(id); free(dsml); if (!ok) return loaded; } return loaded; } #ifdef DS4_SERVER_TEST static void kv_fill_header(uint8_t h[KV_CACHE_FIXED_HEADER], uint8_t quant_bits, uint8_t reason, uint8_t ext_flags, uint32_t tokens, uint32_t hits, uint32_t ctx_size, uint64_t created_at, uint64_t last_used, uint64_t payload_bytes) { ds4_kvstore_fill_header(h, 0, quant_bits, reason, ext_flags, tokens, hits, ctx_size, created_at, last_used, payload_bytes); } #endif static bool kv_read_header(FILE *fp, kv_entry *e, uint32_t *text_bytes) { return ds4_kvstore_read_header(fp, e, text_bytes); } static void kv_cache_restore_tool_memory_for_messages(server *s, const chat_msgs *msgs) { if (!s || s->disable_exact_dsml_tool_replay || !s->kv.enabled || !msgs) return; stop_list wanted = {0}; collect_tool_call_ids(msgs, &wanted); if (wanted.len == 0) return; /* Tool replay payloads are stored next to KV checkpoints; keep them model * scoped too, since token positions and graph state are not portable across * Flash/Pro shapes even when the rendered chat text is identical. */ uint8_t model_id = s->engine ? (uint8_t)ds4_engine_model_id(s->engine) : 0; DIR *d = opendir(s->kv.dir); if (!d) { id_list_free(&wanted); return; } struct dirent *de; while ((de = readdir(d)) != NULL) { char sha[41]; if (!sha_hex_name(de->d_name, sha)) continue; (void)sha; char *path = path_join(s->kv.dir, de->d_name); FILE *fp = fopen(path, "rb"); free(path); if (!fp) continue; kv_entry hdr = {0}; uint32_t text_bytes = 0; bool ok = kv_read_header(fp, &hdr, &text_bytes); uint64_t skip = (uint64_t)text_bytes + hdr.payload_bytes; if (ok && hdr.model_id == model_id && (hdr.ext_flags & KV_EXT_TOOL_MAP) && skip <= (uint64_t)INT64_MAX && fseeko(fp, (off_t)skip, SEEK_CUR) == 0) { kv_tool_map_load_from_pos(s, fp, &wanted); } fclose(fp); } closedir(d); id_list_free(&wanted); } #ifdef DS4_SERVER_TEST static double kv_entry_eviction_score(const kv_entry *e, const ds4_tokens *live, uint64_t now, const ds4_kvstore_eviction_context *incoming) { return ds4_kvstore_entry_eviction_score(e, live, now, incoming); } #endif #ifdef DS4_SERVER_TEST static void kv_cache_evict(kv_disk_cache *kc, const ds4_tokens *live, uint64_t extra_bytes, const ds4_kvstore_eviction_context *incoming) { ds4_kvstore_evict(kc, live, extra_bytes, incoming); } #endif static void kv_cache_log_cb(void *ud, ds4_kvstore_log_type type, const char *msg) { (void)ud; ds4_log_type stype = DS4_LOG_KVCACHE; if (type == DS4_KVSTORE_LOG_DEFAULT) stype = DS4_LOG_DEFAULT; else if (type == DS4_KVSTORE_LOG_WARNING) stype = DS4_LOG_WARNING; server_log(stype, "%s", msg); } static bool kv_cache_open(kv_disk_cache *kc, const char *dir, uint64_t budget_mb, bool reject_different_quant, kv_cache_options opt) { return ds4_kvstore_open(kc, dir, budget_mb, reject_different_quant, opt, "ds4-server", kv_cache_log_cb, NULL); } static void kv_cache_close(kv_disk_cache *kc) { ds4_kvstore_close(kc); } static char *render_tokens_text(ds4_engine *engine, const ds4_tokens *tokens, size_t *out_len) { return ds4_kvstore_render_tokens_text(engine, tokens, out_len); } static bool byte_prefix_match(const char *text, size_t text_len, const char *prefix, size_t prefix_len) { return ds4_kvstore_byte_prefix_match(text, text_len, prefix, prefix_len); } static void tokens_copy_prefix(ds4_tokens *dst, const ds4_tokens *src, int n) { ds4_kvstore_tokens_copy_prefix(dst, src, n); } static void build_prompt_from_exact_prefix_and_text_suffix( ds4_engine *engine, const ds4_tokens *exact_prefix, const char *suffix_text, ds4_tokens *out) { ds4_kvstore_build_prompt_from_exact_prefix_and_text_suffix( engine, exact_prefix, suffix_text, out); } static int kv_cache_store_len(const kv_disk_cache *kc, int tokens) { return ds4_kvstore_store_len(kc, tokens); } static int kv_cache_chat_anchor_pos(const kv_disk_cache *kc, const ds4_tokens *prompt, int user_token_id, int assistant_token_id) { return ds4_kvstore_chat_anchor_pos(kc, prompt, user_token_id, assistant_token_id); } static int kv_cache_continued_store_target(const kv_disk_cache *kc, int live_tokens) { return ds4_kvstore_continued_store_target(kc, live_tokens); } /* A same-text-prefix file can be reused by a larger context, but not by a * smaller one: the payload was validated against the context capacity recorded * in the file. If the existing file cannot be used by this server, replace it * so this context can still populate its own cache. */ #ifdef DS4_SERVER_TEST static bool kv_cache_file_size_fits(const kv_disk_cache *kc, uint64_t text_bytes, uint64_t payload_bytes, uint64_t tool_map_bytes, uint64_t *file_bytes_out, uint64_t *required_bytes_out) { return ds4_kvstore_file_size_fits(kc, text_bytes, payload_bytes, tool_map_bytes, file_bytes_out, required_bytes_out); } #endif static bool kv_cache_tool_map_size_cb(void *ud, const char *text, uint64_t *bytes_out) { return kv_tool_map_serialized_size((server *)ud, text, bytes_out); } static bool kv_cache_tool_map_write_cb(void *ud, FILE *fp, const char *text, uint64_t *written_bytes) { return kv_tool_map_write((server *)ud, fp, text, written_bytes); } static int kv_cache_tool_map_load_cb(void *ud, FILE *fp, const void *wanted) { return kv_tool_map_load_from_pos((server *)ud, fp, (const stop_list *)wanted); } static ds4_kvstore_trailer_hooks kv_cache_tool_map_hooks(server *s, const stop_list *wanted) { return (ds4_kvstore_trailer_hooks){ .ud = s, .ext_flag = KV_EXT_TOOL_MAP, .serialized_size = kv_cache_tool_map_size_cb, .write = kv_cache_tool_map_write_cb, .load = kv_cache_tool_map_load_cb, .load_wanted = wanted, }; } static bool kv_cache_store_live_prefix_text(server *s, server_slot *slot, const ds4_tokens *tokens, int store_len, const char *reason, const char *cache_text_override, uint8_t cache_text_ext, const char *cache_text_key) { if (!s || !slot) return false; char err[160] = {0}; ds4_kvstore_trailer_hooks hooks = kv_cache_tool_map_hooks(s, NULL); pthread_mutex_lock(&s->inference_mu); /* The payload contains image-conditioned KV rows, but the disk key and * trailer do not contain image fingerprints. Never let generic image * placeholder tokens become a cache hit for a different image. * sync_image_count covers progress-callback writes during prefill; * checkpoint_image_count covers completed sessions. */ if (ds4_session_has_vision_state(slot->session)) { pthread_mutex_unlock(&s->inference_mu); return false; } pthread_mutex_lock(&s->kv_mu); bool ok = ds4_kvstore_store_live_prefix_text(&s->kv, s->engine, slot->session, tokens, store_len, reason, cache_text_override, cache_text_ext, cache_text_key, &hooks, err, sizeof(err)); pthread_mutex_unlock(&s->kv_mu); pthread_mutex_unlock(&s->inference_mu); return ok; } static bool kv_cache_store_live_prefix(server *s, server_slot *slot, const ds4_tokens *tokens, int store_len, const char *reason) { return kv_cache_store_live_prefix_text(s, slot, tokens, store_len, reason, NULL, 0, NULL); } static void kv_cache_store_current(server *s, server_slot *slot, const char *reason) { if (!s || !slot) return; const ds4_tokens *tokens = ds4_session_tokens(slot->session); if (!tokens) return; char *visible_text = NULL; uint8_t visible_ext = 0; const char *visible_key = NULL; pthread_mutex_lock(&s->tool_mu); if (slot->responses_live.valid && slot->responses_live.live_tokens == tokens->len && slot->responses_live.visible_text && slot->responses_live.visible_text[0]) { visible_text = xstrdup(slot->responses_live.visible_text); visible_ext = KV_EXT_RESPONSES_VISIBLE; visible_key = "responses-visible"; } else if (slot->thinking_live.valid && !slot->thinking_live.token_text_disk_key && slot->thinking_live.live_tokens == tokens->len && slot->thinking_live.visible_text && slot->thinking_live.visible_text[0]) { visible_text = xstrdup(slot->thinking_live.visible_text); visible_ext = KV_EXT_THINKING_VISIBLE; visible_key = "thinking-visible"; } pthread_mutex_unlock(&s->tool_mu); /* A visible live checkpoint can contain hidden reasoning that the client * intentionally does not replay. For disk recovery after a session switch, * key that payload by the visible protocol transcript, not by rendering the * hidden sampled tokens. On load, DS4 restores the hidden KV payload and * tokenizes only the visible suffix that follows this key. */ if (visible_text) { kv_cache_store_live_prefix_text(s, slot, tokens, tokens->len, reason, visible_text, visible_ext, visible_key); free(visible_text); } else { kv_cache_store_live_prefix(s, slot, tokens, tokens->len, reason); } } #ifdef DS4_SERVER_TEST static int kv_cache_suppress_continued_store(kv_disk_cache *kc, int tokens) { return ds4_kvstore_suppress_continued_store(kc, tokens); } static void kv_cache_restore_suppressed_continued(kv_disk_cache *kc, int old_tokens, int suppressed_tokens) { ds4_kvstore_restore_suppressed_continued(kc, old_tokens, suppressed_tokens); } #endif static int kv_cache_slot_continued_target(server *s, server_slot *slot, int live_tokens) { if (!s || !slot) return 0; kv_disk_cache view = s->kv; view.continued_last_store_tokens = slot->continued_last_store_tokens; return kv_cache_continued_store_target(&view, live_tokens); } static void kv_cache_slot_note_store(server_slot *slot, int tokens) { if (slot && tokens > slot->continued_last_store_tokens) { slot->continued_last_store_tokens = tokens; } } static int kv_cache_slot_suppress_continued(server *s, server_slot *slot, int tokens) { if (kv_cache_slot_continued_target(s, slot, tokens) != tokens) return -1; int old = slot->continued_last_store_tokens; kv_cache_slot_note_store(slot, tokens); return old; } static void kv_cache_slot_restore_suppressed(server_slot *slot, int old_tokens, int suppressed_tokens) { if (slot && old_tokens >= 0 && slot->continued_last_store_tokens == suppressed_tokens) { slot->continued_last_store_tokens = old_tokens; } } static void kv_cache_discard_failed_disk_entry(server *s, server_slot *slot, const char *path) { if (!s || !slot || !path) return; pthread_mutex_lock(&s->kv_mu); if (unlink(path) == 0) { server_log(DS4_LOG_KVCACHE, "ds4-server: kv cache discarded reason=prefill-failed file=%s", path); } else if (errno != ENOENT) { server_log(DS4_LOG_WARNING, "ds4-server: kv cache failed to discard prefill-failed file=%s: %s", path, strerror(errno)); } pthread_mutex_unlock(&s->kv_mu); slot->continued_last_store_tokens = 0; pthread_mutex_lock(&s->inference_mu); ds4_session_invalidate(slot->session); pthread_mutex_unlock(&s->inference_mu); } static void kv_cache_maybe_store_continued(server *s, server_slot *slot) { if (!s || !slot) return; kv_disk_cache *kc = &s->kv; const ds4_tokens *tokens = ds4_session_tokens(slot->session); if (!tokens) return; const int target = kv_cache_slot_continued_target(s, slot, tokens->len); if (target == 0) return; if (kv_cache_store_live_prefix(s, slot, tokens, target, "continued")) { (void)kc; kv_cache_slot_note_store(slot, target); } } #ifdef DS4_SERVER_TEST static int kv_cache_find_text_prefix(kv_disk_cache *kc, const char *prompt_text, int quant_bits, int ctx_size) { return ds4_kvstore_find_text_prefix(kc, prompt_text, 0, quant_bits, ctx_size); } #endif static int kv_cache_try_load_text(server *s, server_slot *slot, const char *prompt_text, ds4_tokens *effective_prompt, char **loaded_path_out, uint8_t *loaded_ext_flags_out, bool responses_protocol) { if (!s || !slot) return 0; if (loaded_path_out) *loaded_path_out = NULL; if (loaded_ext_flags_out) *loaded_ext_flags_out = 0; ds4_kvstore_load_result lr = {0}; ds4_kvstore_trailer_hooks hooks = kv_cache_tool_map_hooks(s, NULL); pthread_mutex_lock(&s->inference_mu); /* Disk payloads intentionally carry no image identity. If this slot held * vision state, discard it before restoring a text-only checkpoint so the * next sync does not reject the fresh payload as a stale image match. */ if (ds4_session_has_vision_state(slot->session)) { ds4_session_invalidate(slot->session); } pthread_mutex_lock(&s->kv_mu); int loaded = ds4_kvstore_try_load_text(&s->kv, s->engine, slot->session, prompt_text, effective_prompt, &lr, &hooks, responses_protocol); pthread_mutex_unlock(&s->kv_mu); pthread_mutex_unlock(&s->inference_mu); if (loaded > 0) { if (loaded_path_out && lr.path) *loaded_path_out = xstrdup(lr.path); if (loaded_ext_flags_out) *loaded_ext_flags_out = lr.ext_flags; } ds4_kvstore_load_result_free(&lr); return loaded; } static int kv_cache_try_load(server *s, server_slot *slot, const request *req, ds4_tokens *effective_prompt, char **loaded_path_out, uint8_t *loaded_ext_flags_out) { return kv_cache_try_load_text(s, slot, req ? req->prompt_text : NULL, effective_prompt, loaded_path_out, loaded_ext_flags_out, req && req->api == API_RESPONSES); } /* A text-only suffix tokenizer would turn image markers into literal text. * Keep the exact live tokens and splice each new image's already-built token * block into the suffix. Historical image positions follow the live frontier, * which may retain reasoning omitted from a client's visible replay. */ static bool build_live_prompt_suffix(server *s, server_slot *slot, const request *req, const char *suffix, ds4_tokens *out) { const ds4_tokens *live = ds4_session_tokens(slot->session); if (!live || !suffix || req->image_count > 16) return false; ds4_vision_span spans[16]; size_t old_count = req->image_count; for (size_t i = 0; i < req->image_count; i++) { if (!req->image_markers) return false; spans[i] = req->images[i]; if (old_count == req->image_count && strstr(suffix, req->image_markers[i])) old_count = i; } if (!ds4_session_rebase_vision_state(slot->session, spans, old_count)) return false; ds4_tokens prompt = {0}; ds4_tokens_copy(&prompt, live); const char *cursor = suffix; const int wrapper = ds4_engine_is_glm_dsa(s->engine) ? 1 : 0; for (size_t i = old_count; i < req->image_count; i++) { const char *marker = strstr(cursor, req->image_markers[i]); int64_t start = (int64_t)req->images[i].token_start - wrapper; uint64_t end = (uint64_t)req->images[i].token_start + req->images[i].embedding.token_count + wrapper; if (!marker || start < 0 || end > (uint64_t)req->prompt.len) { ds4_tokens_free(&prompt); return false; } char *text = xstrndup(cursor, (size_t)(marker - cursor)); ds4_tokenize_rendered_chat(s->engine, text, &prompt); free(text); spans[i].token_start = (uint32_t)(prompt.len + wrapper); for (int64_t p = start; p < (int64_t)end; p++) ds4_tokens_push(&prompt, req->prompt.v[p]); cursor = marker + strlen(req->image_markers[i]); } ds4_tokenize_rendered_chat(s->engine, cursor, &prompt); if (!ds4_session_vision_prefix_matches(slot->session, spans, req->image_count)) { ds4_tokens_free(&prompt); return false; } for (size_t i = 0; i < req->image_count; i++) req->images[i].token_start = spans[i].token_start; ds4_tokens_free(out); *out = prompt; return true; } /* ========================================================================= * Live-state reuse probe. * * Single source of truth for "would this request reuse this slot's live KV * state?", evaluated in the same tier order the execution ladder * (generate_job) materializes: * * responses-visible -> responses-tool-output -> anthropic-tool-output -> * memory-rewind (GLM only) -> memory-token -> thinking-visible -> * memory-text * * Both slot routing (job_slot_score, under dispatch) and the execution * ladder consume this probe, so routing decisions and execution-time reuse * can never disagree about the conditions. The probe is deliberately * cheap: a token-prefix compare, id-list matches, and byte-prefix memcmps * against remembered visible transcripts or the per-slot cached rendered * text. It never tokenizes (that happens once, at materialization time, * for the selected tier only) and never detokenizes (slot_refresh_live_text * maintains the rendered-text cache after each job). * * The tiers, and why they bind a request to a slot without an exact token * prefix match: * * - responses-visible: other clients send the full visible transcript on * every turn even though the API semantics still make the request a * continuation; hidden reasoning may be live in KV but absent from the * replay by design. The request's rendered text must begin with the * visible transcript remembered at the live frontier. * - responses-tool-output: some clients send just the new tool outputs * after a tool call; the call_id set itself is the protocol binding to * the previous live assistant output. * - anthropic-tool-output: /v1/messages has no server-side response object, * but tool_use_id is a precise continuation handle inside a live local * agent loop. * - memory-rewind (GLM only): the prompt is a strict truncation of the * checkpoint; the live KV is rewound and the final prompt token * reevaluated. * - memory-token: the checkpoint is an exact token prefix of the prompt. * - thinking-visible: after a tool-less thinking answer the next prompt * omits the hidden reasoning by design; the remembered visible transcript * is the key that keeps the sampled hidden KV. * - memory-text: the checkpoint's rendered text is a byte prefix of the * request text. Resent history is re-tokenized, and canonical BPE/DSML * tokenization need not reproduce the sampled token ids; the live graph * is authoritative, so the checkpoint tokens are kept verbatim and only * the suffix text is tokenized (full-prompt BPE may have merged across * the byte boundary). * * Locking: slot_probe_reuse_locked() requires tool_mu held (dispatch holds * it while scoring); slot_probe_reuse() is the worker-path wrapper. * ========================================================================= */ static int live_prefix_rewind_target(bool backend_can_rewind, int old_pos, int prompt_len, int common); typedef enum { REUSE_NONE = 0, REUSE_RESPONSES_VISIBLE, REUSE_RESPONSES_TOOL_OUTPUT, REUSE_ANTHROPIC_TOOL_OUTPUT, REUSE_MEMORY_REWIND, REUSE_MEMORY_TOKEN, REUSE_THINKING_VISIBLE, REUSE_MEMORY_TEXT, } slot_reuse_kind; typedef struct { slot_reuse_kind kind; int reuse_tokens; /* live tokens reusable; 0 iff REUSE_NONE */ size_t suffix_off; /* text tiers: byte offset of the new suffix text */ int matched_ids; /* tool-output tiers: number of bound call ids */ } slot_reuse; static slot_reuse slot_probe_reuse_locked(server *s, server_slot *slot, const request *req) { slot_reuse pr = { REUSE_NONE, 0, 0, 0 }; if (!s || !slot || !slot->session || !req) return pr; if (!ds4_session_checkpoint_valid(slot->session)) return pr; const ds4_tokens *live = ds4_session_tokens(slot->session); if (!live || live->len <= 0) return pr; const int live_pos = live->len; /* Vision gate: live KV rows may be conditioned on images whose * fingerprints are invisible to text/token prefixes, so no live tier * may be selected when the request's images do not extend the slot's. * Fingerprint-prefix (not exact state, not token positions) lets a text * checkpoint serve a request appending new images and tolerates shifts * from hidden reasoning -- build_live_prompt_suffix() rebases positions * and re-validates at materialization, turning mismatches into a safe * miss. Baking the check into the probe keeps routing and execution in * agreement and stops image-mismatched requests from evicting good * checkpoints. */ if (!ds4_session_vision_fingerprint_prefix_matches(slot->session, req->images, req->image_count)) return pr; const char *ptext = req->prompt_text; const size_t plen = ptext ? strlen(ptext) : 0; /* Visible tiers use upstream 81a7658 normalization: image markers carry * request-local nonces, so the remembered visible transcript is stored * with markers masked to \036 (visible_prompt_key) plus byte offsets. * Compare in that normalized space and require image-offset prefix * match; raw memcmp would miss whenever nonces differ. Normalization * preserves byte offsets, so suffix_off applies to the raw prompt_text. */ if (req->api == API_RESPONSES && ptext) { visible_image_key vimages; char *vkey = visible_prompt_key(req, ptext, &vimages); if (vkey) { size_t vlen = strlen(vkey); bool ok = slot->responses_live.valid && slot->responses_live.live_tokens == live_pos && slot->responses_live.visible_text && slot->responses_live.visible_len < vlen && visible_image_prefix_matches(&vimages, &slot->responses_live.images, slot->responses_live.visible_len) && byte_prefix_match(vkey, vlen, slot->responses_live.visible_text, slot->responses_live.visible_len); if (ok) { pr.kind = REUSE_RESPONSES_VISIBLE; pr.reuse_tokens = live_pos; pr.suffix_off = slot->responses_live.visible_len; free(vkey); return pr; } free(vkey); } } if (req->api == API_RESPONSES && req->responses_live_suffix_text && req->responses_live_call_ids.len > 0 && responses_live_matches_request_locked(slot, &req->responses_live_call_ids, live_pos)) { pr.kind = REUSE_RESPONSES_TOOL_OUTPUT; pr.reuse_tokens = live_pos; pr.matched_ids = req->responses_live_call_ids.len; return pr; } if (req->api == API_ANTHROPIC && req->anthropic_live_suffix_text && req->anthropic_live_call_ids.len > 0 && anthropic_live_matches_request_locked(slot, &req->anthropic_live_call_ids, live_pos)) { pr.kind = REUSE_ANTHROPIC_TOOL_OUTPUT; pr.reuse_tokens = live_pos; pr.matched_ids = req->anthropic_live_call_ids.len; return pr; } const int common = ds4_session_common_prefix(slot->session, &req->prompt); const bool token_image_prefix = ds4_session_vision_prefix_matches( slot->session, req->images, req->image_count); const int rewind_to = live_prefix_rewind_target( ds4_engine_is_glm_dsa(s->engine), live_pos, req->prompt.len, common); if (rewind_to >= 0 && token_image_prefix) { pr.kind = REUSE_MEMORY_REWIND; pr.reuse_tokens = rewind_to; return pr; } if (common == live_pos && req->prompt.len >= live_pos && token_image_prefix) { pr.kind = REUSE_MEMORY_TOKEN; pr.reuse_tokens = common; return pr; } if (req->kind == REQ_CHAT && req->api != API_RESPONSES && ptext) { visible_image_key vimages; char *vkey = visible_prompt_key(req, ptext, &vimages); if (vkey) { size_t vlen = strlen(vkey); bool ok = slot->thinking_live.valid && slot->thinking_live.live_tokens == live_pos && slot->thinking_live.visible_text && slot->thinking_live.visible_len < vlen && visible_image_prefix_matches(&vimages, &slot->thinking_live.images, slot->thinking_live.visible_len) && byte_prefix_match(vkey, vlen, slot->thinking_live.visible_text, slot->thinking_live.visible_len); if (ok) { pr.kind = REUSE_THINKING_VISIBLE; pr.reuse_tokens = live_pos; pr.suffix_off = slot->thinking_live.visible_len; free(vkey); return pr; } free(vkey); } } if (ptext && slot->live_text && slot->live_text_pos == live_pos && slot->live_text_len <= plen && byte_prefix_match(ptext, plen, slot->live_text, slot->live_text_len)) { pr.kind = REUSE_MEMORY_TEXT; pr.reuse_tokens = live_pos; pr.suffix_off = slot->live_text_len; return pr; } return pr; } static slot_reuse slot_probe_reuse(server *s, server_slot *slot, const request *req) { pthread_mutex_lock(&s->tool_mu); slot_reuse pr = slot_probe_reuse_locked(s, slot, req); pthread_mutex_unlock(&s->tool_mu); return pr; } /* A tool-output-only continuation carries no replayable history and has no disk * key: the live frontier is the only way to serve it. When the probe selected * a live tier but materialization failed (unrepresentable suffix, image * identity accounting mismatch, failed rewind), the request must be rejected * exactly like a missing frontier instead of being answered from a * context-free prompt built from the tool outputs alone. */ static bool live_continuation_unavailable(const request *req, bool materialized) { if (!req || materialized) return false; if (req->api == API_ANTHROPIC) return req->anthropic_requires_live_tool_state; if (req->api == API_RESPONSES) return req->responses_requires_live_tool_state; return false; } /* Refresh the cached rendered text of the slot's checkpoint. Called by the * slot worker after each job - the only place the checkpoint is mutated - so * the memory-text probe stays a pure memcmp and never detokenizes under * tool_mu. */ static void slot_refresh_live_text(server *s, server_slot *slot) { if (!slot) return; free(slot->live_text); slot->live_text = NULL; slot->live_text_len = 0; slot->live_text_pos = 0; if (!s || !slot->session || !ds4_session_checkpoint_valid(slot->session)) { return; } const ds4_tokens *live = ds4_session_tokens(slot->session); if (!live || live->len <= 0) return; slot->live_text = render_tokens_text(s->engine, live, &slot->live_text_len); if (slot->live_text) slot->live_text_pos = live->len; } /* ========================================================================= * Trace Diagnostics. * ========================================================================= * * The human transcript is not enough to debug prompt-cache misses. The model * may generate text that is semantically accepted as a tool call, while the * next OpenAI request re-renders a slightly different canonical DSML block. * That creates a token mismatch even if the conversation "looks" continuous. * * When --trace is enabled we therefore record the exact cache decision and a * small token window around the first mismatch between the live KV checkpoint * and the incoming prompt. Normal server logs stay compact; trace files get * enough data to diagnose tokenizer-boundary and canonicalization problems. */ #define TRACE_CACHE_BEFORE 8 #define TRACE_CACHE_AFTER 8 #define TRACE_CACHE_WINDOW (TRACE_CACHE_BEFORE + 1 + TRACE_CACHE_AFTER) typedef struct { bool valid; int old_pos; int prompt_len; int common; int rewind_to; int start; int count; int live_id[TRACE_CACHE_WINDOW]; int prompt_id[TRACE_CACHE_WINDOW]; } trace_cache_diag; static void trace_cache_capture( trace_cache_diag *d, const ds4_tokens *live, const ds4_tokens *prompt, int old_pos, int common) { memset(d, 0, sizeof(*d)); d->valid = true; d->rewind_to = -1; d->old_pos = old_pos; d->prompt_len = prompt ? prompt->len : 0; d->common = common; const int live_len = live ? live->len : 0; const int prompt_len = prompt ? prompt->len : 0; int max_len = live_len > prompt_len ? live_len : prompt_len; int start = common - TRACE_CACHE_BEFORE; if (start < 0) start = 0; int end = common + TRACE_CACHE_AFTER + 1; if (end > max_len) end = max_len; if (end < start) end = start; d->start = start; d->count = end - start; if (d->count > TRACE_CACHE_WINDOW) d->count = TRACE_CACHE_WINDOW; for (int i = 0; i < d->count; i++) { int pos = start + i; d->live_id[i] = live && pos < live->len ? live->v[pos] : -1; d->prompt_id[i] = prompt && pos < prompt->len ? prompt->v[pos] : -1; } } static const char *trace_cache_miss_reason(const trace_cache_diag *d) { if (!d || !d->valid) return "unknown"; if (d->old_pos == 0) return "no-live-checkpoint"; if (d->rewind_to >= 0) return "live-prefix-rewind"; if (d->common != d->old_pos) return "token-mismatch"; if (d->prompt_len < d->old_pos) return "incoming-prompt-shorter-than-live-checkpoint"; return "live-prefix-match"; } static bool trace_cache_memory_reusable(const trace_cache_diag *d) { return d && d->valid && (d->rewind_to >= 0 || (d->old_pos > 0 && d->common == d->old_pos && d->prompt_len >= d->old_pos)); } static void trace_write_escaped_bytes(FILE *fp, const char *p, size_t len) { static const char hex[] = "0123456789abcdef"; fputc('"', fp); for (size_t i = 0; i < len; i++) { unsigned char c = (unsigned char)p[i]; if (c == '"' || c == '\\') { fputc('\\', fp); fputc((char)c, fp); } else if (c == '\n') { fputs("\\n", fp); } else if (c == '\r') { fputs("\\r", fp); } else if (c == '\t') { fputs("\\t", fp); } else if (c < 0x20 || c == 0x7f) { fputs("\\x", fp); fputc(hex[c >> 4], fp); fputc(hex[c & 15], fp); } else { fputc((char)c, fp); } } fputc('"', fp); } static void trace_write_token(FILE *fp, ds4_engine *engine, int token) { if (token < 0) { fputs("- ", fp); return; } size_t len = 0; char *piece = ds4_token_text(engine, token, &len); fprintf(fp, "%d ", token); trace_write_escaped_bytes(fp, piece, len); free(piece); } static void trace_write_cache_diag( server *s, const trace_cache_diag *d, const tool_replay_stats *tool_replay, int cached, const char *cache_source, int disk_cached, const char *disk_path) { fprintf(s->trace, "\n--- cache decision ---\n" "live_tokens_before: %d\n" "prompt_tokens: %d\n" "live_prompt_common: %d\n" "memory_token_reusable: %d\n" "memory_miss_reason: %s\n" "tool_replay: mem=%d disk=%d canonical=%d missing_ids=%d\n" "cache_source: %s\n" "cached_tokens: %d\n" "disk_cached_tokens: %d\n", d && d->valid ? d->old_pos : 0, d && d->valid ? d->prompt_len : 0, d && d->valid ? d->common : 0, trace_cache_memory_reusable(d) ? 1 : 0, trace_cache_miss_reason(d), tool_replay ? tool_replay->mem : 0, tool_replay ? tool_replay->disk : 0, tool_replay ? tool_replay->canonical : 0, tool_replay ? tool_replay->missing_ids : 0, cache_source ? cache_source : "none", cached, disk_cached); if (disk_path && disk_path[0]) fprintf(s->trace, "disk_cache_file: %s\n", disk_path); if (!d || !d->valid || d->old_pos == 0 || trace_cache_memory_reusable(d)) { return; } fprintf(s->trace, "\nfirst_mismatch_token: %d\n" "token_window: [%d..%d)\n", d->common, d->start, d->start + d->count); for (int i = 0; i < d->count; i++) { int pos = d->start + i; int live = d->live_id[i]; int prompt = d->prompt_id[i]; const char *mark; if (live < 0) mark = "prompt-only"; else if (prompt < 0) mark = "live-only"; else mark = live == prompt ? "==" : "!="; fprintf(s->trace, "%7d %-11s live ", pos, mark); trace_write_token(s->trace, s->engine, live); fputs(" | prompt ", s->trace); trace_write_token(s->trace, s->engine, prompt); fputc('\n', s->trace); } } static int live_prefix_rewind_target(bool backend_can_rewind, int old_pos, int prompt_len, int common) { if (!backend_can_rewind || prompt_len <= 1 || prompt_len >= old_pos) return -1; if (common != prompt_len) return -1; return prompt_len - 1; } static void trace_time(FILE *fp) { time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); char buf[32]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); fputs(buf, fp); } static uint64_t trace_begin( server *s, const job *j, int cached, int effective_prompt_tokens, const trace_cache_diag *cache_diag, const char *cache_source, int disk_cached, const char *disk_path) { if (!s->trace) return 0; pthread_mutex_lock(&s->trace_mu); uint64_t id = ++s->trace_seq; fprintf(s->trace, "\n===== request %llu ", (unsigned long long)id); trace_time(s->trace); fprintf(s->trace, " =====\nkind: %s\nmodel: %s\nstream: %d\ntools: %d\nthink_mode: %s\nprompt_tokens: %d\neffective_prompt_tokens: %d\ncached_tokens: %d\nmax_tokens: %d\ntemperature: %.3f\ntop_k: %d\ntop_p: %.3f\nmin_p: %.3f\nignore_eos: %d\nseed: %llu\n", j->req.kind == REQ_CHAT ? "chat" : "completion", j->req.model ? j->req.model : "", j->req.stream ? 1 : 0, j->req.has_tools ? 1 : 0, ds4_think_mode_name(j->req.think_mode), j->req.prompt.len, effective_prompt_tokens, cached, j->req.max_tokens, j->req.temperature, j->req.top_k, j->req.top_p, j->req.min_p, j->req.ignore_eos ? 1 : 0, (unsigned long long)j->req.seed); fprintf(s->trace, "stream_include_usage: %d\n", j->req.stream_include_usage ? 1 : 0); trace_write_cache_diag(s, cache_diag, &j->req.tool_replay, cached, cache_source, disk_cached, disk_path); if (j->req.raw_body) { fputs("\n--- raw request json ---\n", s->trace); fputs(j->req.raw_body, s->trace); if (!j->req.raw_body[0] || j->req.raw_body[strlen(j->req.raw_body) - 1] != '\n') { fputc('\n', s->trace); } } if (j->req.prompt_text) { fputs("\n--- rendered prompt ---\n", s->trace); fputs(j->req.prompt_text, s->trace); if (!j->req.prompt_text[0] || j->req.prompt_text[strlen(j->req.prompt_text) - 1] != '\n') { fputc('\n', s->trace); } } fputs("\n--- generated text ---\n", s->trace); fflush(s->trace); pthread_mutex_unlock(&s->trace_mu); return id; } static void trace_piece(server *s, uint64_t id, const char *piece, size_t len) { if (!s->trace || !id || !piece || !len) return; pthread_mutex_lock(&s->trace_mu); fwrite(piece, 1, len, s->trace); fflush(s->trace); pthread_mutex_unlock(&s->trace_mu); } static void trace_event(server *s, uint64_t id, const char *fmt, ...) { if (!s->trace || !id) return; pthread_mutex_lock(&s->trace_mu); fputs("\n\n--- trace: ", s->trace); va_list ap; va_start(ap, fmt); vfprintf(s->trace, fmt, ap); va_end(ap); fputs(" ---\n\n", s->trace); fflush(s->trace); pthread_mutex_unlock(&s->trace_mu); } static void trace_finish( server *s, uint64_t id, const request *r, const char *final_finish, int completion, bool saw_tool_start, bool saw_tool_end, const char *parsed_content, const char *parsed_reasoning, const tool_calls *parsed_calls, double elapsed) { if (!s->trace || !id) return; pthread_mutex_lock(&s->trace_mu); fprintf(s->trace, "\n\n--- parsed message ---\nfinish: %s\ngenerated_tokens: %d\ndsml_start: %d\ndsml_end: %d\nelapsed_sec: %.3f\n", final_finish, completion, saw_tool_start ? 1 : 0, saw_tool_end ? 1 : 0, elapsed); if (r->kind == REQ_CHAT) { if (parsed_reasoning && parsed_reasoning[0]) { fputs("\nreasoning:\n", s->trace); fputs(parsed_reasoning, s->trace); fputc('\n', s->trace); } if (parsed_content && parsed_content[0]) { fputs("\ncontent:\n", s->trace); fputs(parsed_content, s->trace); fputc('\n', s->trace); } for (int i = 0; i < parsed_calls->len; i++) { const tool_call *tc = &parsed_calls->v[i]; fprintf(s->trace, "\ntool_call[%d]:\nid: %s\nname: %s\narguments:\n%s\n", i, tc->id ? tc->id : "", tc->name ? tc->name : "", tc->arguments ? tc->arguments : ""); } } fprintf(s->trace, "\n===== end request %llu =====\n", (unsigned long long)id); fflush(s->trace); pthread_mutex_unlock(&s->trace_mu); } typedef struct { server *srv; server_slot *slot; req_kind kind; int prompt_tokens; int cached_tokens; char ctx[48]; const char *phase; bool has_tools; bool responses_protocol; double t0; double last_t; int last_current; bool seen; /* SSE keepalive during long prefill: send HTTP/SSE headers ahead of * generation and emit a `:` comment line every few seconds so HTTP/TCP * idle timeouts on the client side don't close the connection while the * server is busy doing prefill. */ int fd; bool stream; bool enable_cors; bool headers_sent; bool stream_failed; double last_keepalive; job *request_job; } server_prefill_progress; static void request_ctx_span(char *buf, size_t len, int cached, int prompt) { int suffix = prompt - cached; if (suffix < 0) suffix = 0; snprintf(buf, len, "%d..%d:%d", cached, prompt, suffix); } static void log_flags(char *buf, size_t len, bool responses_protocol, bool tools, bool thinking, bool dsml_start, bool dsml_end) { size_t used = 0; buf[0] = '\0'; #define ADD_FLAG(name) do { \ int n = snprintf(buf + used, used < len ? len - used : 0, "%s%s", used ? " " : "", name); \ if (n > 0) used += (size_t)n; \ } while (0) if (responses_protocol) ADD_FLAG("RESPPROTO"); if (tools) ADD_FLAG("TOOLS"); if (thinking) ADD_FLAG("THINKING"); if (dsml_start) ADD_FLAG("DSML_START"); if (dsml_end) ADD_FLAG("DSML_END"); #undef ADD_FLAG } static void log_decode_progress(req_kind kind, int prompt_tokens, int completion, bool responses_protocol, bool tools, bool thinking, bool dsml_start, bool dsml_end, double decode_t0, double *last_t, int *last_completion) { const double now = now_sec(); const double elapsed = now - decode_t0; const double interval_s = now - *last_t; const int interval_tokens = completion - *last_completion; const double chunk_tps = interval_s > 0.0 ? (double)interval_tokens / interval_s : 0.0; const double avg_tps = elapsed > 0.0 ? (double)completion / elapsed : 0.0; char ctx[48]; request_ctx_span(ctx, sizeof(ctx), prompt_tokens + *last_completion, prompt_tokens + completion); char flags[80]; log_flags(flags, sizeof(flags), responses_protocol, tools, thinking, dsml_start, dsml_end); server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s gen=%d%s%s decoding chunk=%.2f t/s avg=%.2f t/s %.3fs", kind == REQ_CHAT ? "chat" : "completion", ctx, completion, flags[0] ? " " : "", flags, chunk_tps, avg_tps, elapsed); *last_t = now; *last_completion = completion; } typedef struct { bool inside; char tail[8]; /* Long enough for "". */ int tail_len; } thinking_state; static bool thinking_tail_ends_with(const thinking_state *st, const char *s) { int n = (int)strlen(s); return st->tail_len >= n && !memcmp(st->tail + st->tail_len - n, s, (size_t)n); } static void thinking_state_feed(thinking_state *st, const char *p, size_t len) { if (!st || !p) return; for (size_t i = 0; i < len; i++) { if (st->tail_len == (int)sizeof(st->tail)) { memmove(st->tail, st->tail + 1, sizeof(st->tail) - 1); st->tail_len--; } st->tail[st->tail_len++] = p[i]; if (thinking_tail_ends_with(st, "")) st->inside = true; else if (thinking_tail_ends_with(st, "")) st->inside = false; } } static thinking_state thinking_state_from_prompt(const request *r) { thinking_state st = {0}; if (r && r->prompt_text) { thinking_state_feed(&st, r->prompt_text, strlen(r->prompt_text)); } else if (r && ds4_think_mode_enabled(r->think_mode)) { st.inside = true; } return st; } /* A completed tool block inside unclosed reasoning can be recovered without * predicting what the model will emit after an injected close marker. Keep a * short overlap until the opening appears, then wait for its matching end. */ static bool complete_tool_call_inside_thinking(server_model_syntax syntax, const char *text, size_t len, size_t *scan_from) { if (!text || !scan_from) return false; if (*scan_from > len) *scan_from = len; const char *start = find_any_tool_start(text + *scan_from); if (!start) { const size_t hold = 80; *scan_from = len > hold ? len - hold : 0; return false; } *scan_from = (size_t)(start - text); if (!find_any_tool_end(start)) return false; char *content = NULL, *reasoning = NULL; tool_calls calls = {0}; bool complete = parse_generated_message_ex_for_syntax(syntax, start, false, &content, &reasoning, &calls) && calls.len > 0; free(content); free(reasoning); tool_calls_free(&calls); return complete; } static int server_eval_token(server *s, server_slot *slot, int token, char *err, size_t errlen); static char *rendered_chat_system_region(const char *prompt_text) { if (!prompt_text) return xstrdup(""); const char *p = prompt_text; const char *bos = "<|begin▁of▁sentence|>"; const size_t bos_len = strlen(bos); if (!strncmp(p, bos, bos_len)) p += bos_len; const char *max_prefix = ds4_think_max_prefix(); const size_t max_prefix_len = strlen(max_prefix); if (max_prefix_len && !strncmp(p, max_prefix, max_prefix_len)) { p += max_prefix_len; } while (*p && isspace((unsigned char)*p)) p++; const char *user = strstr(p, "<|User|>"); const char *assistant = strstr(p, "<|Assistant|>"); const char *end = NULL; if (user && assistant) end = user < assistant ? user : assistant; else end = user ? user : assistant; if (!end) end = p + strlen(p); while (end > p && isspace((unsigned char)end[-1])) end--; return xstrndup(p, (size_t)(end - p)); } static char *build_invalid_dsml_tool_error_suffix(const request *r, const thinking_state *thinking, const char *detail) { char *system = rendered_chat_system_region(r ? r->prompt_text : NULL); buf tool_error = {0}; buf_puts(&tool_error, "Tool error: invalid DSML tool call"); if (detail && detail[0]) { buf_puts(&tool_error, ": "); buf_puts(&tool_error, detail); } buf_puts(&tool_error, "\nThe previous assistant output was not executed because the DSML syntax was malformed. " "Emit a new valid DSML tool call, or answer normally if no tool is needed."); if (system && system[0]) { buf_puts(&tool_error, "\n\nSystem prompt reminder:\n"); buf_puts(&tool_error, system); } buf suffix = {0}; if (r && ds4_think_mode_enabled(r->think_mode) && thinking && thinking->inside) { buf_puts(&suffix, ""); } buf_puts(&suffix, "<|end▁of▁sentence|><|User|>"); append_tool_result_text(&suffix, tool_error.ptr ? tool_error.ptr : ""); buf_puts(&suffix, "<|Assistant|>"); buf_puts(&suffix, r && ds4_think_mode_enabled(r->think_mode) ? "" : ""); free(system); buf_free(&tool_error); return buf_take(&suffix); } static char *build_invalid_glm_tool_error_suffix(const request *r, const thinking_state *thinking, const char *detail) { buf tool_error = {0}; buf_puts(&tool_error, "Tool error: invalid GLM tool call"); if (detail && detail[0]) { buf_puts(&tool_error, ": "); buf_puts(&tool_error, detail); } buf_puts(&tool_error, "\nThe previous assistant output was not executed because the " " syntax was malformed. Emit a new valid , " "or answer normally if no tool is needed."); buf suffix = {0}; if (r && ds4_think_mode_enabled(r->think_mode) && thinking && thinking->inside) { buf_puts(&suffix, ""); } buf_puts(&suffix, "<|observation|>"); append_glm_tool_response_text(&suffix, tool_error.ptr ? tool_error.ptr : ""); buf_puts(&suffix, "<|assistant|>"); buf_puts(&suffix, r && ds4_think_mode_enabled(r->think_mode) ? "" : ""); buf_free(&tool_error); return buf_take(&suffix); } static char *build_invalid_qwen_tool_error_suffix(const request *r, const thinking_state *thinking, const char *detail) { buf tool_error = {0}; buf_puts(&tool_error, "Tool error: invalid Qwen tool call"); if (detail && detail[0]) { buf_puts(&tool_error, ": "); buf_puts(&tool_error, detail); } buf_puts(&tool_error, "\nThe previous assistant output was not executed because the " " syntax was malformed. Emit a new valid " ", or answer normally if no tool is needed."); buf suffix = {0}; if (r && ds4_think_mode_enabled(r->think_mode) && thinking && thinking->inside) { buf_puts(&suffix, "\n\n\n"); } buf_puts(&suffix, "<|im_end|>\n<|im_start|>user"); append_qwen_tool_response(&suffix, tool_error.ptr ? tool_error.ptr : "", tool_error.len); buf_puts(&suffix, "<|im_end|>\n"); append_qwen_generation_prompt(&suffix, r && ds4_think_mode_enabled(r->think_mode)); buf_free(&tool_error); return buf_take(&suffix); } static char *build_invalid_tool_call_error_suffix(const request *r, const thinking_state *thinking, const char *detail) { if (r && r->model_syntax == SERVER_MODEL_SYNTAX_GLM) { return build_invalid_glm_tool_error_suffix(r, thinking, detail); } if (r && r->model_syntax == SERVER_MODEL_SYNTAX_QWEN) { return build_invalid_qwen_tool_error_suffix(r, thinking, detail); } return build_invalid_dsml_tool_error_suffix(r, thinking, detail); } static int server_next_prefill_slot_locked(const server *s) { if (!s || s->slot_count <= 0) return -1; for (int n = 1; n <= s->slot_count; n++) { int id = (s->last_prefill_slot + n) % s->slot_count; if (s->slots[id].prefill_waiting) return id; } return -1; } static bool server_prefill_enter(server *s, server_slot *slot) { if (!s || !slot || slot_job_cancelled(slot)) return false; if (!s->batched_mode) { pthread_mutex_lock(&s->inference_mu); if (slot_job_cancelled(slot)) { pthread_mutex_unlock(&s->inference_mu); return false; } return true; } pthread_mutex_lock(&s->model_mu); slot->prefill_waiting = true; pthread_cond_broadcast(&s->model_cv); while (!g_stop_requested && !slot_job_cancelled(slot) && (s->model_busy || s->decode_pending > 0 || server_next_prefill_slot_locked(s) != slot->id)) { pthread_cond_wait(&s->model_cv, &s->model_mu); } if (g_stop_requested || slot_job_cancelled(slot)) { slot->prefill_waiting = false; pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); return false; } slot->prefill_waiting = false; s->last_prefill_slot = slot->id; s->model_busy = true; pthread_mutex_unlock(&s->model_mu); pthread_mutex_lock(&s->inference_mu); return true; } static void server_prefill_leave(server *s) { if (!s) return; pthread_mutex_unlock(&s->inference_mu); if (!s->batched_mode) return; pthread_mutex_lock(&s->model_mu); s->model_busy = false; pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); } static int server_prefill_quantum_for(const server *s, bool generation_active) { int quantum = generation_active ? s->mixed_prefill_quantum : s->idle_prefill_quantum > 2048 ? s->idle_prefill_quantum : 2048; if (generation_active && quantum < 1024 && s->engine && ds4_engine_is_glm53(s->engine)) { quantum = 1024; } return quantum; } static int server_prefill_quantum(server *s) { pthread_mutex_lock(&s->model_mu); bool generation_active = s->active_generations > 0; pthread_mutex_unlock(&s->model_mu); return server_prefill_quantum_for(s, generation_active); } /* Synchronize one resident slot without monopolizing the model executor. A * non-matching prompt is first rebuilt to one quantum; a matching checkpoint * advances from its current frontier. Absolute positions remain session-local, * so compressor alignment is independent of scheduler order. */ static int server_session_sync(server *s, server_slot *slot, const ds4_tokens *prompt, char *err, size_t errlen) { if (!s || !slot || !prompt) return 1; if (!s->batched_mode) { if (!server_prefill_enter(s, slot)) return DS4_SESSION_SYNC_INTERRUPTED; int rc = ds4_session_sync(slot->session, prompt, err, errlen); server_prefill_leave(s); return rc; } pthread_mutex_lock(&s->inference_mu); int live = ds4_session_pos(slot->session); int common = ds4_session_common_prefix(slot->session, prompt); pthread_mutex_unlock(&s->inference_mu); int done = common == live && prompt->len >= live ? live : 0; bool called = false; while (!g_stop_requested && !slot_job_cancelled(slot) && (!called || done < prompt->len)) { int quantum = server_prefill_quantum(s); int target = done + quantum; if (target > prompt->len || target < done) target = prompt->len; if (target <= 0) target = prompt->len; ds4_tokens prefix = *prompt; prefix.len = target; if (!server_prefill_enter(s, slot)) return DS4_SESSION_SYNC_INTERRUPTED; int rc = ds4_session_sync(slot->session, &prefix, err, errlen); if (rc == 0) done = ds4_session_pos(slot->session); server_prefill_leave(s); called = true; if (rc != 0) return rc; if (done >= prompt->len) return 0; if (done < target) { if (err && errlen) snprintf(err, errlen, "prefill made no progress"); return 1; } } return (g_stop_requested || slot_job_cancelled(slot)) ? DS4_SESSION_SYNC_INTERRUPTED : 0; } static int server_multimodal_resume_frontier(int live, int common, int prompt_len, bool image_state_matches) { return common == live && prompt_len >= live && image_state_matches ? live : 0; } static int server_multimodal_resume_pos(ds4_session *session, const ds4_tokens *prompt, const ds4_vision_span *images, size_t image_count) { if (!session || !prompt) return 0; const int live = ds4_session_pos(session); const int common = ds4_session_common_prefix(session, prompt); return server_multimodal_resume_frontier( live, common, prompt->len, ds4_session_vision_prefix_matches(session, images, image_count)); } static int server_session_sync_multimodal(server *s, server_slot *slot, const ds4_tokens *prompt, const ds4_vision_span *images, size_t image_count, char *err, size_t errlen) { if (!image_count) return server_session_sync(s, slot, prompt, err, errlen); if (!s || !slot || !prompt || !images) return 1; if (!s->batched_mode) { if (!server_prefill_enter(s, slot)) return DS4_SESSION_SYNC_INTERRUPTED; int rc = ds4_session_sync_multimodal(slot->session, prompt, images, image_count, err, errlen); server_prefill_leave(s); return rc; } /* Start at the exact live frontier when both tokens and image identities * match. Starting at zero made the first scheduling slice truncate a * perfectly reusable long checkpoint, forcing a complete refill. */ pthread_mutex_lock(&s->inference_mu); int done = server_multimodal_resume_pos(slot->session, prompt, images, image_count); pthread_mutex_unlock(&s->inference_mu); bool called = false; while (!g_stop_requested && !slot_job_cancelled(slot) && (!called || done < prompt->len)) { int quantum = server_prefill_quantum(s); int target = done + quantum; if (target > prompt->len || target < done) target = prompt->len; if (target <= 0) target = prompt->len; for (size_t i = 0; i < image_count; i++) { uint64_t end = (uint64_t)images[i].token_start + images[i].embedding.token_count; if ((uint64_t)target > images[i].token_start && (uint64_t)target < end) { target = end > (uint64_t)prompt->len ? prompt->len : (int)end; } } size_t prefix_images = 0; while (prefix_images < image_count) { uint64_t end = (uint64_t)images[prefix_images].token_start + images[prefix_images].embedding.token_count; if (end > (uint64_t)target) break; prefix_images++; } ds4_tokens prefix = *prompt; prefix.len = target; if (!server_prefill_enter(s, slot)) return DS4_SESSION_SYNC_INTERRUPTED; int rc = ds4_session_sync_multimodal(slot->session, &prefix, images, prefix_images, err, errlen); if (rc == 0) done = ds4_session_pos(slot->session); server_prefill_leave(s); called = true; if (rc != 0) return rc; if (done >= prompt->len) return 0; if (done < target) { if (err && errlen) snprintf(err, errlen, "prefill made no progress"); return 1; } } return (g_stop_requested || slot_job_cancelled(slot)) ? DS4_SESSION_SYNC_INTERRUPTED : 0; } static bool append_rendered_suffix_to_live_session(server *s, server_slot *slot, const char *suffix, int *tokens_appended, char *err, size_t errlen) { if (tokens_appended) *tokens_appended = 0; if (!s || !slot || !suffix || !suffix[0]) return true; const ds4_tokens *live = ds4_session_tokens(slot->session); if (!live) { if (err && errlen) snprintf(err, errlen, "live session is unavailable"); return false; } ds4_tokens target = {0}; build_prompt_from_exact_prefix_and_text_suffix(s->engine, live, suffix, &target); const int before = ds4_session_pos(slot->session); bool ok = server_session_sync(s, slot, &target, err, errlen) == 0; if (ok && tokens_appended) { int delta = ds4_session_pos(slot->session) - before; *tokens_appended = delta > 0 ? delta : 0; } ds4_tokens_free(&target); return ok; } /* A recurrent cache cannot always be truncated in place. Match the agent's * boundary handling and rebuild the retained prefix when rewind invalidates * it, retaining image conditioning as well. */ static int server_generation_rewind(server *s, server_slot *slot, const request *r, int pos, char *err, size_t errlen) { pthread_mutex_lock(&s->inference_mu); ds4_session_rewind(slot->session, pos); ds4_tokens prefix = {0}; ds4_tokens_copy(&prefix, ds4_session_tokens(slot->session)); bool rebuild = ds4_session_common_prefix(slot->session, &prefix) != prefix.len; pthread_mutex_unlock(&s->inference_mu); int rc = rebuild ? server_session_sync_multimodal(s, slot, &prefix, r->images, r->image_count, err, errlen) : 0; ds4_tokens_free(&prefix); return rc; } static bool continue_after_invalid_dsml(server *s, server_slot *slot, const request *r, const thinking_state *thinking, const char *detail, int *tokens_appended, char *err, size_t errlen) { char *suffix = build_invalid_tool_call_error_suffix(r, thinking, detail); bool ok = append_rendered_suffix_to_live_session(s, slot, suffix, tokens_appended, err, errlen); free(suffix); return ok; } static int server_decode_budget(int requested, int generated, int room) { if (requested <= generated || room <= 0) return 0; int remaining = requested - generated; return remaining < room ? remaining : room; } static bool should_remember_thinking_checkpoint(const request *r, const thinking_state *thinking, const char *finish) { if (!r || r->kind != REQ_CHAT) return false; /* Qwen Chat Completions clients may omit reasoning even with tools. * Remember an alternative visible key without changing exact replay. * Actual tool calls have their own checkpoint path at the call site. */ const bool qwen_chat = r->model_syntax == SERVER_MODEL_SYNTAX_QWEN && r->api != API_RESPONSES && r->api != API_ANTHROPIC; if ((r->has_tools || r->prompt_preserves_reasoning) && !qwen_chat) return false; if (!ds4_think_mode_enabled(r->think_mode)) return false; if (finish && (!strcmp(finish, "error") || !strcmp(finish, "length"))) return false; if (thinking && thinking->inside) return false; return true; } static void log_tool_calls_summary(const char *ctx, const tool_calls *calls, bool responses_protocol) { if (!calls || calls->len == 0) return; buf names = {0}; buf ids = {0}; for (int i = 0; i < calls->len; i++) { if (i) buf_putc(&names, ','); if (i) buf_putc(&ids, ','); buf_puts(&names, calls->v[i].name ? calls->v[i].name : "?"); buf_puts(&ids, calls->v[i].id ? calls->v[i].id : "?"); } char flags[32]; log_flags(flags, sizeof(flags), responses_protocol, false, false, false, false); server_log(DS4_LOG_TOOL, "ds4-server: tool calls ctx=%s%s%s n=%d raw_tool_text=%d ids=[%s] names=[%s]", ctx, flags[0] ? " " : "", flags, calls->len, calls->raw_tool_text && calls->raw_tool_text[0] ? 1 : 0, ids.ptr ? ids.ptr : "", names.ptr ? names.ptr : ""); buf_free(&ids); buf_free(&names); } static void server_progress_cb(void *ud, const char *event, int current, int total) { server_prefill_progress *p = ud; if (!p || !event || job_cancelled(p->request_job)) return; const bool is_chunk = strcmp(event, "prefill_chunk") == 0; const bool is_display = strcmp(event, "prefill_display") == 0; if (!is_chunk && !is_display) return; double now = now_sec(); /* Keep the HTTP/SSE connection alive while prefill runs. We write the SSE * response headers the first time the callback fires and then emit a * comment line (`:` prefix, ignored by SSE clients) every few seconds. * A failed write marks the job cancelled; the session callback then stops * prefill at the next backend-safe boundary. */ if (p->stream && p->fd >= 0 && !p->stream_failed) { if (!p->headers_sent) { p->headers_sent = true; if (sse_headers(p->fd, p->enable_cors)) { p->last_keepalive = now; } else { p->stream_failed = true; job_mark_cancelled(p->request_job); return; } } else if (now - p->last_keepalive >= 5.0) { static const char ka[] = ": prefill\n\n"; if (send_all(p->fd, ka, sizeof(ka) - 1)) { p->last_keepalive = now; } else { p->stream_failed = true; job_mark_cancelled(p->request_job); return; } } } if (is_display) return; double elapsed = now - p->t0; if (p->seen && current == p->last_current) { if (p->srv && p->slot && current > p->cached_tokens) { kv_cache_maybe_store_continued(p->srv, p->slot); } return; } int display_start = p->cached_tokens; if (display_start < 0 || display_start > p->prompt_tokens) display_start = 0; int display_total = p->prompt_tokens - display_start; if (display_total <= 0) { display_start = 0; display_total = p->prompt_tokens > total ? p->prompt_tokens : total; } int display_current = current - display_start; if (display_current < 0) display_current = 0; if (display_current > display_total) display_current = display_total; double pct = display_total > 0 ? 100.0 * (double)display_current / (double)display_total : 100.0; double avg_tps = elapsed > 0.0 ? (double)display_current / elapsed : 0.0; int interval_tokens = p->seen ? current - p->last_current : 0; if (interval_tokens < 0) interval_tokens = 0; double interval_s = p->seen ? now - p->last_t : 0.0; double chunk_tps = interval_s > 0.0 ? (double)interval_tokens / interval_s : 0.0; p->last_current = current; p->last_t = now; p->seen = true; char flags[64]; log_flags(flags, sizeof(flags), p->responses_protocol, p->has_tools, false, false, false); const char *phase = p->phase ? p->phase : "prefill"; server_log(DS4_LOG_PREFILL, "ds4-server: %s ctx=%s%s%s %s chunk %d/%d (%.1f%%) chunk=%.2f t/s avg=%.2f t/s %.3fs", p->kind == REQ_CHAT ? "chat" : "completion", p->ctx, flags[0] ? " " : "", flags, phase, display_current, display_total, pct, chunk_tps, avg_tps, elapsed); if (p->srv && p->slot && current > p->cached_tokens) { kv_cache_maybe_store_continued(p->srv, p->slot); } } static void send_prefill_failure_response(server *s, const job *j, const server_prefill_progress *progress, const char *ctx, const char *flags, const char *err) { const char *kind = j->req.kind == REQ_CHAT ? "chat" : "completion"; if (j->req.stream && progress && progress->headers_sent) { if (progress->stream_failed) { server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s%s%s prefill failed after stream closed: %s", kind, ctx, flags && flags[0] ? " " : "", flags && flags[0] ? flags : "", err); return; } if (!sse_error_event(j->fd, &j->req, err)) { server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s%s%s prefill SSE error failed: %s", kind, ctx, flags && flags[0] ? " " : "", flags && flags[0] ? flags : "", err); } return; } http_error(j->fd, s->enable_cors, 500, err); } /* The bytes render_qwen_chat_prompt_text() appends after this turn's * generation prompt, so the live checkpoint and the next prompt agree byte for * byte. */ static char *build_qwen_assistant_suffix(const request *r, const char *content, const char *reasoning, bool with_reasoning, const tool_calls *calls) { buf suffix = {0}; if (r && ds4_think_mode_enabled(r->think_mode)) { if (with_reasoning) append_trimmed_text(&suffix, reasoning); buf_puts(&suffix, "\n\n\n"); } buf body = {0}; append_trimmed_text(&body, content); buf_puts(&suffix, body.ptr ? body.ptr : ""); append_qwen_tool_calls_text(&suffix, calls, body.len > 0, r ? &r->tool_orders : NULL); buf_free(&body); buf_puts(&suffix, "<|im_end|>"); return buf_take(&suffix); } static char *build_tool_checkpoint_suffix(const request *r, const char *content, const char *reasoning, const tool_calls *calls) { const server_model_syntax syntax = r ? r->model_syntax : SERVER_MODEL_SYNTAX_DEEPSEEK; if (syntax == SERVER_MODEL_SYNTAX_QWEN) { return build_qwen_assistant_suffix(r, content, reasoning, true, calls); } buf suffix = {0}; if (r && ds4_think_mode_enabled(r->think_mode)) { buf_puts(&suffix, reasoning ? reasoning : ""); buf_puts(&suffix, ""); } buf_puts(&suffix, content ? content : ""); append_tool_calls_text_for_syntax(&suffix, syntax, calls, r ? &r->tool_orders : NULL); if (syntax != SERVER_MODEL_SYNTAX_GLM) { buf_puts(&suffix, "<|end▁of▁sentence|>"); } return buf_take(&suffix); } static char *build_responses_visible_assistant_suffix(const request *r, const char *content, const char *reasoning, const tool_calls *calls) { const server_model_syntax syntax = r ? r->model_syntax : SERVER_MODEL_SYNTAX_DEEPSEEK; if (syntax == SERVER_MODEL_SYNTAX_QWEN) { return build_qwen_assistant_suffix(r, content, reasoning, r && r->reasoning_summary_emit && calls && calls->len > 0, calls); } buf suffix = {0}; /* This suffix mirrors what a Responses client can replay, not necessarily * every token in KV. Hidden reasoning stays live in the session unless the * next client replay is expected to include it. In practice, pi replays * reasoning summaries for tool-call turns, but not for final assistant * answers; Codex currently requests no summaries at all. So only include * reasoning in the remembered visible prefix when this assistant turn ended * in tool calls. A client that does replay final-answer reasoning will not * match this visible shortcut and can still use exact token-prefix replay. */ if (r && ds4_think_mode_enabled(r->think_mode)) { if (r->reasoning_summary_emit && calls && calls->len > 0) { buf_puts(&suffix, reasoning ? reasoning : ""); } buf_puts(&suffix, ""); } buf_puts(&suffix, content ? content : ""); append_tool_calls_text_for_syntax(&suffix, syntax, calls, r ? &r->tool_orders : NULL); if (syntax != SERVER_MODEL_SYNTAX_GLM) { buf_puts(&suffix, "<|end▁of▁sentence|>"); } return buf_take(&suffix); } /* Clients can omit old assistant reasoning from later prompts, including Qwen * text answers with tool schemas. The sampled live graph still contains the * reasoning bytes, so the next request would miss the session cache even though * the visible conversation prefix is logically the same. * * prompt-without-final- + + visible-content + eos * * is exactly the visible prefix that render_chat_prompt_text() will produce on * the next turn. Do not rebuild the KV cache to erase hidden reasoning here: * that caused long post-answer pauses and threw away useful sampled state. * Instead, remember the visible bytes as a key for the current sampled frontier. * The next request can then continue from live KV while tokenizing only the new * visible suffix. */ static char *build_thinking_visible_text(const request *r, const char *content) { if (!r || !r->prompt_text) return NULL; if (!ds4_think_mode_enabled(r->think_mode)) return NULL; size_t pt_len = strlen(r->prompt_text); if (r->model_syntax == SERVER_MODEL_SYNTAX_QWEN) { /* the next request renders this turn as an empty think block plus the * trimmed content, closed by <|im_end|> */ const char *gen = "<|im_start|>assistant\n\n"; const size_t gen_len = strlen(gen); if (pt_len < gen_len || memcmp(r->prompt_text + pt_len - gen_len, gen, gen_len) != 0) return NULL; buf visible = {0}; buf_append(&visible, r->prompt_text, pt_len); buf_puts(&visible, "\n\n\n"); append_trimmed_text(&visible, content ? content : ""); buf_puts(&visible, "<|im_end|>\n"); return buf_take(&visible); } const char *think_tag = ""; size_t tag_len = strlen(think_tag); if (pt_len < tag_len || memcmp(r->prompt_text + pt_len - tag_len, think_tag, tag_len) != 0) { return NULL; } buf visible = {0}; buf_append(&visible, r->prompt_text, pt_len - tag_len); if (r->model_syntax == SERVER_MODEL_SYNTAX_GLM) { buf_puts(&visible, ""); append_trimmed_text(&visible, content); } else { buf_puts(&visible, ""); buf_puts(&visible, content ? content : ""); buf_puts(&visible, "<|end▁of▁sentence|>"); } return buf_take(&visible); } static void remember_thinking_checkpoint(server *s, server_slot *slot, const job *j, const char *ctx, uint64_t trace_id, const char *content, bool visible_continuation) { char *visible = build_thinking_visible_text(&j->req, content); if (!visible) return; thinking_live_remember(s, slot, visible, &j->req); pthread_mutex_lock(&s->tool_mu); slot->thinking_live.token_text_disk_key = j->req.prompt_preserves_reasoning && !visible_continuation; pthread_mutex_unlock(&s->tool_mu); server_log(DS4_LOG_KVCACHE, "ds4-server: thinking live checkpoint remembered ctx=%s live=%d visible=%zu", ctx, ds4_session_pos(slot->session), strlen(visible)); trace_event(s, trace_id, "thinking live checkpoint remembered: live=%d visible=%zu", ds4_session_pos(slot->session), strlen(visible)); free(visible); } /* Match clients that omit reasoning, while keeping the exact sampled KV. * Tool decoding stops at , BEFORE the assistant end token. Leave * <|im_end|> out of the visible key so the next suffix actually evaluates it. */ static char *build_qwen_tool_turn_visible_text(const request *r, const char *finish, bool inside_thinking, const char *content, const tool_calls *calls) { if (!r || !calls || calls->len == 0) return NULL; if (r->kind != REQ_CHAT || r->image_count != 0) return NULL; if (r->api == API_RESPONSES || r->api == API_ANTHROPIC) return NULL; if (r->model_syntax != SERVER_MODEL_SYNTAX_QWEN) return NULL; if (!r->prompt_text || !r->prompt_text[0]) return NULL; if (!calls->raw_tool_text || !calls->raw_tool_text[0]) return NULL; /* Use the decode finish, not the parser's promoted tool_calls status: * repaired/truncated output and unclosed reasoning are not this frontier. */ if (!finish || strcmp(finish, "tool_calls") || inside_thinking) return NULL; char *suffix = build_qwen_assistant_suffix(r, content, NULL, false, calls); buf visible = {0}; buf_puts(&visible, r->prompt_text); buf_append(&visible, suffix, strlen(suffix) - strlen("<|im_end|>")); free(suffix); return buf_take(&visible); } static bool remember_qwen_tool_turn_visible_checkpoint(server *s, server_slot *slot, job *j, const char *ctx, const char *finish, bool inside_thinking, const char *content, const tool_calls *calls) { char *visible = build_qwen_tool_turn_visible_text(&j->req, finish, inside_thinking, content, calls); if (!visible) return false; thinking_live_remember(s, slot, visible, &j->req); server_log(DS4_LOG_KVCACHE, "ds4-server: qwen tool-turn visible checkpoint remembered ctx=%s live=%d visible=%zu", ctx, ds4_session_pos(slot->session), strlen(visible)); free(visible); return true; } /* After a successful tool-call finish, make the live checkpoint match what the * next request will render. Usually that is just the exact DSML remembered by * tool id. If a client sends a tool call without an id we know, the fallback * renderer still builds valid DSML from JSON, and this function either rewrites * the short suffix in place or reloads an older disk checkpoint before replay. */ static void canonicalize_tool_checkpoint(server *s, server_slot *slot, job *j, const char *ctx, uint64_t trace_id, const char *content, const char *reasoning, const tool_calls *calls) { if (!calls || calls->len == 0 || !j->req.prompt_text) return; char *suffix_text = build_tool_checkpoint_suffix(&j->req, content, reasoning, calls); buf rendered = {0}; buf_puts(&rendered, j->req.prompt_text); buf_puts(&rendered, suffix_text); ds4_tokens canonical = {0}; ds4_tokenize_rendered_chat(s->engine, rendered.ptr ? rendered.ptr : "", &canonical); const int live_len = ds4_session_pos(slot->session); const int common = ds4_session_common_prefix(slot->session, &canonical); if (common == live_len && canonical.len == live_len) goto done; size_t live_text_len = 0; char *live_text = render_tokens_text(s->engine, ds4_session_tokens(slot->session), &live_text_len); if (live_text_len == rendered.len && (live_text_len == 0 || memcmp(live_text, rendered.ptr, live_text_len) == 0)) { /* The graph already represents the bytes the next request will render. * Token-level canonicalization would only replace a valid sampled * history with a different BPE spelling of the same transcript. */ free(live_text); goto done; } free(live_text); if (j->req.image_count != 0) { /* Rebuilding through the text-only disk cache would either lose the * vision embeddings or restore rows for an unverified image. Keep the * correctly conditioned sampled frontier instead. */ server_log(DS4_LOG_WARNING, "ds4-server: multimodal tool checkpoint canonicalization skipped ctx=%s common=%d live=%d canonical=%d reason=preserve-image-state", ctx, common, live_len, canonical.len); trace_event(s, trace_id, "multimodal tool checkpoint canonicalization skipped: common=%d live=%d canonical=%d", common, live_len, canonical.len); goto done; } if (common < j->req.prompt.len) { trace_event(s, trace_id, "tool checkpoint canonicalization skipped: common=%d prompt=%d live=%d canonical=%d", common, j->req.prompt.len, live_len, canonical.len); goto done; } char err[160] = {0}; pthread_mutex_lock(&s->inference_mu); ds4_session_rewrite_result rr = ds4_session_rewrite_from_common(slot->session, &canonical, common, err, sizeof(err)); pthread_mutex_unlock(&s->inference_mu); if (rr == DS4_SESSION_REWRITE_OK) { server_log(DS4_LOG_KVCACHE, "ds4-server: tool checkpoint canonicalized ctx=%s common=%d live=%d canonical=%d", ctx, common, live_len, canonical.len); trace_event(s, trace_id, "tool checkpoint canonicalized: common=%d live=%d canonical=%d", common, live_len, canonical.len); } else if (rr == DS4_SESSION_REWRITE_REBUILD_NEEDED) { /* The generated DSML suffix and the canonical prompt share a prefix, * but the generated tail is too large to overwrite safely inside the * live raw-window ring. Prefer an older disk checkpoint over replaying * a very long conversation from token zero. */ char *path = NULL; ds4_tokens effective = {0}; int loaded = kv_cache_try_load_text(s, slot, rendered.ptr ? rendered.ptr : "", &effective, &path, NULL, false); if (loaded == 0) { pthread_mutex_lock(&s->inference_mu); ds4_session_invalidate(slot->session); pthread_mutex_unlock(&s->inference_mu); } char sync_err[160] = {0}; const ds4_tokens *sync_prompt = loaded > 0 ? &effective : &canonical; char rebuild_ctx[48]; request_ctx_span(rebuild_ctx, sizeof(rebuild_ctx), loaded, sync_prompt->len); int replay_tokens = sync_prompt->len - loaded; if (replay_tokens < 0) replay_tokens = sync_prompt->len; int canonical_tail_tokens = canonical.len - common; if (canonical_tail_tokens < 0) canonical_tail_tokens = canonical.len; int discarded_live_tokens = live_len - common; if (discarded_live_tokens < 0) discarded_live_tokens = 0; const char *source = loaded > 0 ? "disk" : "full"; const double rebuild_t0 = now_sec(); server_log(DS4_LOG_KVCACHE, "ds4-server: tool checkpoint canonicalization needs %d tokens rebuild ctx=%s request_ctx=%s reason=canonical-tail-rewrite tail=%d discard=%d common=%d live=%d target=%d cached=%d source=%s%s%s", replay_tokens, rebuild_ctx, ctx, canonical_tail_tokens, discarded_live_tokens, common, live_len, canonical.len, loaded, source, path ? " file=" : "", path ? path : ""); server_prefill_progress rebuild_progress = { .srv = s, .slot = slot, .kind = j->req.kind, .prompt_tokens = sync_prompt->len, .cached_tokens = loaded, .phase = "tool checkpoint rebuild", .has_tools = j->req.has_tools, .t0 = rebuild_t0, .fd = j->fd, .stream = j->req.stream, .enable_cors = s->enable_cors, .request_job = j, /* Tool checkpoint rebuild only runs after the response stream is * already in flight, so the SSE headers were sent long ago. * Pre-arm the flag so the progress callback only emits keepalive * comments and never tries to write a second set of headers. */ .headers_sent = true, }; snprintf(rebuild_progress.ctx, sizeof(rebuild_progress.ctx), "%s", rebuild_ctx); ds4_session_set_progress(slot->session, server_progress_cb, &rebuild_progress); ds4_session_set_display_progress(slot->session, server_progress_cb, &rebuild_progress); if (server_session_sync(s, slot, sync_prompt, sync_err, sizeof(sync_err)) == 0) { ds4_session_set_progress(slot->session, NULL, NULL); ds4_session_set_display_progress(slot->session, NULL, NULL); const double rebuild_sec = now_sec() - rebuild_t0; if (loaded > 0) { server_log(DS4_LOG_KVCACHE, "ds4-server: tool checkpoint rebuild done ctx=%s request_ctx=%s source=disk cached=%d replay=%d target=%d %.3fs", rebuild_ctx, ctx, loaded, replay_tokens, canonical.len, rebuild_sec); trace_event(s, trace_id, "tool checkpoint canonicalized via disk: common=%d live=%d canonical=%d cached=%d file=%s", common, live_len, canonical.len, loaded, path ? path : ""); } else { server_log(DS4_LOG_KVCACHE, "ds4-server: tool checkpoint rebuild done ctx=%s request_ctx=%s source=full cached=0 replay=%d target=%d %.3fs", rebuild_ctx, ctx, replay_tokens, canonical.len, rebuild_sec); trace_event(s, trace_id, "tool checkpoint canonicalized via rebuild: common=%d live=%d canonical=%d reason=%s", common, live_len, canonical.len, err); } } else { ds4_session_set_progress(slot->session, NULL, NULL); ds4_session_set_display_progress(slot->session, NULL, NULL); server_log(DS4_LOG_KVCACHE, "ds4-server: tool checkpoint rebuild failed ctx=%s request_ctx=%s source=%s cached=%d replay=%d target=%d error=\"%s\"", rebuild_ctx, ctx, source, loaded, replay_tokens, canonical.len, sync_err); trace_event(s, trace_id, "tool checkpoint canonicalization failed after rebuild request: %s", sync_err); } ds4_tokens_free(&effective); free(path); } else { server_log(DS4_LOG_KVCACHE, "ds4-server: tool checkpoint canonicalization failed ctx=%s common=%d live=%d canonical=%d error=\"%s\"", ctx, common, live_len, canonical.len, err); trace_event(s, trace_id, "tool checkpoint canonicalization failed: %s", err); } done: ds4_tokens_free(&canonical); buf_free(&rendered); free(suffix_text); } static bool should_canonicalize_tool_checkpoint(const server *s, const tool_calls *calls) { if (!calls || calls->len == 0) return false; if (s && !s->disable_exact_dsml_tool_replay && calls->raw_tool_text && calls->raw_tool_text[0]) { return false; } return true; } static void server_generation_enter(server *s) { if (!s || !s->batched_mode) return; pthread_mutex_lock(&s->model_mu); s->active_generations++; pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); } static void server_generation_leave(server *s) { if (!s || !s->batched_mode) return; pthread_mutex_lock(&s->model_mu); if (s->active_generations > 0) s->active_generations--; pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); } /* model_mu must be held. An in-flight batch owns the session until it * completes, but a pending token can be withdrawn without touching backend * state. */ static bool server_cancel_pending_decode_locked(server *s, server_slot *slot) { if (!s || !slot || !slot->decode_pending || slot->decode_in_flight) return false; slot->decode_pending = false; s->decode_pending--; slot->decode_rc = DS4_SESSION_SYNC_INTERRUPTED; snprintf(slot->decode_err, sizeof(slot->decode_err), "client disconnected"); slot->decode_done = true; pthread_cond_broadcast(&s->model_cv); return true; } static int server_eval_tokens(server *s, server_slot *slot, int token, bool speculative, int accepted[2], int *n_accepted, char *err, size_t errlen) { *n_accepted = 0; if (!s || !slot) return 1; if (!s->batched_mode) { if (g_stop_requested || slot_job_cancelled(slot)) { if (err && errlen) snprintf(err, errlen, "%s", g_stop_requested ? "shutdown requested" : "client disconnected"); return DS4_SESSION_SYNC_INTERRUPTED; } pthread_mutex_lock(&s->inference_mu); int rc = ds4_session_eval(slot->session, token, err, errlen); pthread_mutex_unlock(&s->inference_mu); if (rc == 0) { accepted[0] = token; *n_accepted = 1; } return rc; } pthread_mutex_lock(&s->model_mu); if (g_stop_requested || slot_job_cancelled(slot)) { pthread_mutex_unlock(&s->model_mu); if (err && errlen) snprintf(err, errlen, "%s", g_stop_requested ? "shutdown requested" : "client disconnected"); return DS4_SESSION_SYNC_INTERRUPTED; } if (slot->decode_pending || slot->decode_in_flight) { pthread_mutex_unlock(&s->model_mu); if (err && errlen) snprintf(err, errlen, "session already has a decode in flight"); return 1; } slot->decode_token = token; slot->decode_speculative = speculative; slot->decode_accepted_count = 0; slot->decode_rc = 1; slot->decode_err[0] = '\0'; slot->decode_done = false; slot->decode_pending = true; s->decode_pending++; pthread_cond_broadcast(&s->model_cv); while (!slot->decode_done) { const bool client_cancelled = slot_job_cancelled(slot); if ((client_cancelled || g_stop_requested) && server_cancel_pending_decode_locked(s, slot)) { if (!client_cancelled) { snprintf(slot->decode_err, sizeof(slot->decode_err), "shutdown requested"); } break; } /* An in-flight backend call still owns the session. Even during * shutdown or client cancellation, wait for that safe boundary before * the stack-owned job and its cancellation callback can be released. */ pthread_cond_wait(&s->model_cv, &s->model_mu); } int rc = slot->decode_rc; if (g_stop_requested && rc == 0) rc = DS4_SESSION_SYNC_INTERRUPTED; if (rc != 0 && err && errlen) { snprintf(err, errlen, "%s", g_stop_requested ? "shutdown requested" : (slot->decode_err[0] ? slot->decode_err : "decode interrupted")); } slot->decode_done = false; if (rc == 0) { *n_accepted = slot->decode_accepted_count; memcpy(accepted, slot->decode_accepted, (size_t)*n_accepted * sizeof(int)); } pthread_mutex_unlock(&s->model_mu); return rc; } static int server_eval_token(server *s, server_slot *slot, int token, char *err, size_t errlen) { int accepted[2], count; return server_eval_tokens(s, slot, token, false, accepted, &count, err, errlen); } static long server_decode_coalesce_us(void) { long us = 2000; const char *env = getenv("DS4_SERVER_DECODE_COALESCE_US"); if (env && env[0]) { char *end = NULL; long v = strtol(env, &end, 10); if (end != env && *end == '\0' && v >= 0 && v <= 100000) us = v; } return us; } static void timespec_add_us(struct timespec *ts, long us) { if (!ts || us <= 0) return; ts->tv_nsec += (us % 1000000L) * 1000L; ts->tv_sec += us / 1000000L + ts->tv_nsec / 1000000000L; ts->tv_nsec %= 1000000000L; } static void *decode_worker_main(void *arg) { server *s = arg; ds4_decode_item *items = xmalloc((size_t)s->slot_count * sizeof(*items)); server_slot **members = xmalloc((size_t)s->slot_count * sizeof(*members)); int (*accepted)[2] = xmalloc((size_t)s->slot_count * sizeof(*accepted)); int *n_accepted = xmalloc((size_t)s->slot_count * sizeof(*n_accepted)); const long coalesce_us = server_decode_coalesce_us(); const bool log_batches = getenv("DS4_SERVER_BATCH_LOG") != NULL; pthread_mutex_lock(&s->model_mu); for (;;) { while (s->decode_pending == 0 && !s->model_stopping) { pthread_cond_wait(&s->model_cv, &s->model_mu); } if (s->decode_pending == 0 && s->model_stopping) break; if (coalesce_us > 0 && s->decode_pending < s->slot_count) { struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); timespec_add_us(&deadline, coalesce_us); int observed = s->decode_pending; while (!s->model_stopping && s->decode_pending < s->slot_count && s->decode_pending < s->active_generations) { int rc = pthread_cond_timedwait(&s->model_cv, &s->model_mu, &deadline); if (rc == ETIMEDOUT) break; if (s->decode_pending == observed && rc != 0) break; observed = s->decode_pending; } } while (s->model_busy) { pthread_cond_wait(&s->model_cv, &s->model_mu); } if (s->model_stopping && s->decode_pending == 0) break; int count = 0, plain_count = 0; /* Keep sampled/exact requests out of the greedy speculative group. */ for (int speculative = 0; speculative <= 1; speculative++) { for (int i = 0; i < s->slot_count; i++) { server_slot *slot = &s->slots[i]; if (!slot->decode_pending || slot->decode_speculative != (bool)speculative) continue; slot->decode_pending = false; slot->decode_in_flight = true; s->decode_pending--; members[count] = slot; items[count].session = slot->session; items[count].token = slot->decode_token; count++; } if (!speculative) plain_count = count; } if (count == 0) continue; s->model_busy = true; pthread_mutex_unlock(&s->model_mu); char batch_err[160] = {0}; const double batch_t0 = log_batches ? now_sec() : 0.0; pthread_mutex_lock(&s->inference_mu); int rc = plain_count ? ds4_sessions_eval_batch(items, plain_count, batch_err, sizeof(batch_err)) : 0; for (int i = 0; i < plain_count; i++) { accepted[i][0] = items[i].token; n_accepted[i] = 1; } if (rc == 0 && count > plain_count) rc = ds4_sessions_eval_batch_speculative_argmax(items + plain_count, count - plain_count, accepted + plain_count, n_accepted + plain_count, batch_err, sizeof(batch_err)); if (rc != 0) for (int i = 0; i < count; i++) ds4_session_invalidate(items[i].session); pthread_mutex_unlock(&s->inference_mu); if (log_batches) { server_log(DS4_LOG_DEFAULT, "ds4-server: decode batch count=%d elapsed=%.3f ms status=%s", count, (now_sec() - batch_t0) * 1000.0, rc == 0 ? "ok" : "error"); } pthread_mutex_lock(&s->model_mu); s->model_busy = false; for (int i = 0; i < count; i++) { server_slot *slot = members[i]; slot->decode_in_flight = false; slot->decode_rc = rc; if (rc == 0) { slot->decode_accepted_count = n_accepted[i]; memcpy(slot->decode_accepted, accepted[i], (size_t)n_accepted[i] * sizeof(int)); } if (rc != 0) { snprintf(slot->decode_err, sizeof(slot->decode_err), "%s", batch_err[0] ? batch_err : "batched decode failed"); } slot->decode_done = true; } pthread_cond_broadcast(&s->model_cv); } pthread_mutex_unlock(&s->model_mu); free(members); free(items); free(accepted); free(n_accepted); return NULL; } /* Execute one request on the worker-owned session. * * Clients resend full prompts as text. The worker first tries the old exact * token-prefix hit, then a rendered-text prefix hit for the live checkpoint, * then disk text-prefix restart snapshots, then a cold prefill. On text-prefix * hits we build a fresh effective prompt from the checkpoint's exact token * history plus a newly tokenized string suffix; the canonical full-prompt * tokens are not sliced because BPE may merge across the byte boundary. Cold * prompt caching is handled before generation: if the stable checkpoint is * shorter than the full prompt, we prefill to that boundary, store it, and * immediately continue to the real prompt. The live graph therefore always * moves forward. */ static void generate_job_inner(server *s, server_slot *slot, job *j) { char err[160]; err[0] = '\0'; const bool multimodal = j->req.image_count != 0; pthread_mutex_lock(&s->inference_mu); const int old_pos = ds4_session_pos(slot->session); const int common = ds4_session_common_prefix(slot->session, &j->req.prompt); const bool live_vision_match = ds4_session_vision_prefix_matches(slot->session, j->req.images, j->req.image_count); pthread_mutex_unlock(&s->inference_mu); trace_cache_diag cache_diag = {0}; trace_cache_capture(&cache_diag, ds4_session_tokens(slot->session), &j->req.prompt, old_pos, common); ds4_tokens effective_prompt = {0}; const ds4_tokens *prompt_for_sync = &j->req.prompt; const bool responses_protocol = j->req.api == API_RESPONSES; bool responses_live_continuation = false; bool anthropic_live_continuation = false; bool thinking_live_continuation = false; const char *responses_live_match = NULL; int responses_live_match_ids = 0; int anthropic_live_match_ids = 0; /* Responses gets the first chance to continue from live state. This is * the whole point of the API shape: a request that is bound to prior live * output by visible transcript or tool call ids does not need to prove an * exact token-prefix match. Exact token/text/disk matching remains the * fallback when the live state is absent or no longer describes the * request. */ /* Reuse probe: the single source of truth for "does this request reuse * this slot's live state?", shared with slot routing (job_slot_score) so * execution and routing can never disagree. Tier order and conditions * live in slot_probe_reuse_locked(); below we only materialize the tier * the probe selected. */ slot_reuse reuse = slot_probe_reuse(s, slot, &j->req); if (responses_protocol && j->req.responses_requires_live_tool_state && reuse.kind != REUSE_RESPONSES_VISIBLE && reuse.kind != REUSE_RESPONSES_TOOL_OUTPUT) { /* The parser saw a valid live call_id, but by worker execution time the * live frontier no longer matches. Since the request did not replay * the prior assistant call, there is no stateless prefix to match and * no disk key to search by. */ ds4_tokens_free(&effective_prompt); http_error(j->fd, s->enable_cors, 409, "Responses continuation state is not available; retry by replaying the full input history"); return; } if (j->req.api == API_ANTHROPIC && j->req.anthropic_requires_live_tool_state && reuse.kind != REUSE_ANTHROPIC_TOOL_OUTPUT) { ds4_tokens_free(&effective_prompt); http_error(j->fd, s->enable_cors, 409, "Anthropic continuation state is not available; retry by replaying the full messages history"); return; } int cached = reuse.reuse_tokens; const char *cache_source = "none"; /* Whether the selected tier actually produced a reusable live prefix. */ bool live_materialized = false; switch (reuse.kind) { case REUSE_RESPONSES_VISIBLE: if (!build_live_prompt_suffix(s, slot, &j->req, j->req.prompt_text + reuse.suffix_off, &effective_prompt)) { cached = 0; cache_source = "none"; break; } live_materialized = true; cache_source = "responses-visible"; responses_live_continuation = true; responses_live_match = "visible-prefix"; if (responses_live_matches_request(s, slot, &j->req.responses_live_call_ids, old_pos)) { responses_live_match_ids = j->req.responses_live_call_ids.len; } prompt_for_sync = &effective_prompt; break; case REUSE_RESPONSES_TOOL_OUTPUT: if (!build_live_prompt_suffix(s, slot, &j->req, j->req.responses_live_suffix_text, &effective_prompt)) { cached = 0; cache_source = "none"; break; } live_materialized = true; cache_source = "responses-tool-output"; responses_live_continuation = true; responses_live_match = "tool-output-ids"; responses_live_match_ids = reuse.matched_ids; prompt_for_sync = &effective_prompt; break; case REUSE_ANTHROPIC_TOOL_OUTPUT: if (!build_live_prompt_suffix(s, slot, &j->req, j->req.anthropic_live_suffix_text, &effective_prompt)) { cached = 0; cache_source = "none"; break; } live_materialized = true; cache_source = "anthropic-tool-output"; anthropic_live_continuation = true; anthropic_live_match_ids = reuse.matched_ids; prompt_for_sync = &effective_prompt; break; case REUSE_MEMORY_REWIND: { pthread_mutex_lock(&s->inference_mu); ds4_session_rewind(slot->session, reuse.reuse_tokens); /* Rewinding mutates the session, so re-validate before trusting the * frontier: the common prefix must land exactly on the rewind target * and the vision state must still describe the request (upstream's * multimodal hardening). A failed validation falls through to the * disk cache / cold prefill instead of reusing a bad frontier. */ const bool rewind_valid = ds4_session_common_prefix(slot->session, &j->req.prompt) == reuse.reuse_tokens && (!multimodal || ds4_session_vision_state_matches(slot->session, j->req.images, j->req.image_count)); pthread_mutex_unlock(&s->inference_mu); if (rewind_valid) { live_materialized = true; cache_source = "memory-rewind"; cache_diag.rewind_to = reuse.reuse_tokens; server_log(DS4_LOG_KVCACHE, "ds4-server: rewound GLM live prefix from %d to %d; final prompt token will be reevaluated", old_pos, reuse.reuse_tokens); } else { cached = 0; server_log(DS4_LOG_KVCACHE, "ds4-server: GLM live prefix rewind from %d to %d requires rebuild", old_pos, reuse.reuse_tokens); } break; } case REUSE_MEMORY_TOKEN: live_materialized = true; cache_source = "memory-token"; break; case REUSE_THINKING_VISIBLE: if (!build_live_prompt_suffix(s, slot, &j->req, j->req.prompt_text + reuse.suffix_off, &effective_prompt)) { cached = 0; cache_source = "none"; break; } live_materialized = true; cache_source = "thinking-visible"; thinking_live_continuation = true; prompt_for_sync = &effective_prompt; break; case REUSE_MEMORY_TEXT: if (!build_live_prompt_suffix(s, slot, &j->req, j->req.prompt_text + reuse.suffix_off, &effective_prompt)) { cached = 0; cache_source = "none"; break; } live_materialized = true; cache_source = "memory-text"; prompt_for_sync = &effective_prompt; break; case REUSE_NONE: default: cached = 0; break; } if (live_continuation_unavailable(&j->req, live_materialized)) { ds4_tokens_free(&effective_prompt); if (j->req.api == API_ANTHROPIC) { http_error(j->fd, s->enable_cors, 409, "Anthropic continuation state is not available; retry by replaying the full messages history"); } else { http_error(j->fd, s->enable_cors, 409, "Responses continuation state is not available; retry by replaying the full input history"); } return; } int disk_cached = 0; char *disk_cache_path = NULL; uint8_t disk_cache_ext_flags = 0; if (cached == 0 && old_pos > 0) { server_log(DS4_LOG_WARNING, "ds4-server: live kv cache miss%s live=%d prompt=%d common=%d vision=%s reason=%s", responses_protocol ? " RESPPROTO" : "", old_pos, j->req.prompt.len, common, live_vision_match ? "match" : "mismatch", trace_cache_miss_reason(&cache_diag)); } if (multimodal && cached > 0) { server_log(DS4_LOG_KVCACHE, "ds4-server: multimodal live kv hit images=%zu cached=%d prompt=%d identity=fingerprint-match", j->req.image_count, cached, prompt_for_sync->len); } if (cached == 0) slot->continued_last_store_tokens = 0; if (!multimodal && s->kv.enabled && cached == 0 && old_pos >= s->kv.opt.min_tokens) { /* Loading a disk snapshot replaces the live Metal session. Persist the * current checkpoint first, otherwise a cache hit for an older prefix * would silently discard the newer conversation state. */ kv_cache_store_current(s, slot, "evict"); } if (!multimodal && cached == 0) { disk_cached = kv_cache_try_load(s, slot, &j->req, &effective_prompt, &disk_cache_path, &disk_cache_ext_flags); if (disk_cached > 0) { cached = disk_cached; cache_source = "disk-text"; prompt_for_sync = &effective_prompt; } } const bool responses_reasoning_state_preserved = cached > 0 && ((!strcmp(cache_source, "responses-visible") || !strcmp(cache_source, "responses-tool-output")) || (!strcmp(cache_source, "disk-text") && (disk_cache_ext_flags & KV_EXT_RESPONSES_VISIBLE))); const bool responses_visible_replay_without_reasoning = responses_protocol && j->req.responses_requires_live_reasoning && !responses_reasoning_state_preserved; const int prompt_tokens = prompt_for_sync->len; /* OpenAI usage details: the reusable prefix is a cache read, while the * effective prompt suffix evaluated by ds4_session_sync() is written into * the live KV cache and can be reused by the next request. */ j->req.cache_read_tokens = cached; j->req.cache_write_tokens = prompt_tokens > cached ? prompt_tokens - cached : 0; const double t0 = now_sec(); uint64_t trace_id = trace_begin(s, j, cached, prompt_tokens, &cache_diag, cache_source, disk_cached, disk_cache_path); char ctx_span[48]; request_ctx_span(ctx_span, sizeof(ctx_span), cached, prompt_tokens); server_prefill_progress progress = { .srv = s, .slot = slot, .kind = j->req.kind, .prompt_tokens = prompt_tokens, .cached_tokens = cached, .has_tools = j->req.has_tools, .responses_protocol = responses_protocol, .t0 = t0, .fd = j->fd, .stream = j->req.stream, .enable_cors = s->enable_cors, .request_job = j, }; snprintf(progress.ctx, sizeof(progress.ctx), "%s", ctx_span); char req_flags[64]; log_flags(req_flags, sizeof(req_flags), responses_protocol, j->req.has_tools, false, false, false); if (responses_live_continuation) { server_log(DS4_LOG_PREFILL, "ds4-server: responses live continuation RESPPROTO match=%s ids=%d cached=%d prompt=%d", responses_live_match ? responses_live_match : "unknown", responses_live_match_ids, cached, prompt_tokens); } else if (anthropic_live_continuation) { server_log(DS4_LOG_PREFILL, "ds4-server: anthropic live continuation match=tool-output-ids ids=%d cached=%d prompt=%d", anthropic_live_match_ids, cached, prompt_tokens); } else if (thinking_live_continuation) { server_log(DS4_LOG_PREFILL, "ds4-server: thinking live continuation match=visible-prefix cached=%d prompt=%d", cached, prompt_tokens); } if (responses_visible_replay_without_reasoning) { /* The request replays a prior tool-call turn but omits the hidden * reasoning that originally led to it. A live Responses checkpoint, or * a responses-visible disk checkpoint, would preserve that hidden KV. * If neither is available, continue from the visible transcript instead * of surfacing a hard error to the user. This is lower fidelity, but it * lets old / restarted agent sessions recover and is exactly what the * client asked us to prefill. */ server_log(DS4_LOG_WARNING, "ds4-server: responses replay RESPPROTO missing reasoning state; continuing from visible history source=%s cached=%d prompt=%d", cache_source, cached, prompt_tokens); trace_event(s, trace_id, "responses replay missing reasoning state; continuing from visible history source=%s cached=%d", cache_source, cached); } server_log(DS4_LOG_PREFILL, "ds4-server: %s ctx=%s%s%s prompt start", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, req_flags[0] ? " " : "", req_flags); ds4_session_set_progress(slot->session, server_progress_cb, &progress); ds4_session_set_display_progress(slot->session, server_progress_cb, &progress); int cold_store_len = 0; if (!multimodal && cached == 0 && s->kv.enabled && prompt_for_sync->len >= s->kv.opt.min_tokens && s->kv.opt.cold_max_tokens > 0 && prompt_for_sync->len <= s->kv.opt.cold_max_tokens) { const int anchor = kv_cache_chat_anchor_pos(&s->kv, prompt_for_sync, ds4_token_user(s->engine), ds4_token_assistant(s->engine)); cold_store_len = anchor >= s->kv.opt.min_tokens ? anchor : kv_cache_store_len(&s->kv, prompt_for_sync->len); } int suppressed_continued_last = -1; if (cold_store_len >= s->kv.opt.min_tokens) { /* A cold checkpoint can land exactly on the continued-checkpoint * frontier. The prefill progress callback would then write the same * prefix as "continued" while we are intentionally stopping there to * write it as "cold". Mark the frontier as already handled before the * sync reaches it; if the cold write fails, restore the old schedule so * a later continued write can still try. */ suppressed_continued_last = kv_cache_slot_suppress_continued(s, slot, cold_store_len); } if (s->kv.enabled && cold_store_len >= s->kv.opt.min_tokens && cold_store_len < prompt_for_sync->len) { ds4_tokens prefix = {0}; tokens_copy_prefix(&prefix, prompt_for_sync, cold_store_len); if (server_session_sync(s, slot, &prefix, err, sizeof(err)) != 0) { ds4_tokens_free(&prefix); ds4_tokens_free(&effective_prompt); ds4_session_set_progress(slot->session, NULL, NULL); ds4_session_set_display_progress(slot->session, NULL, NULL); kv_cache_slot_restore_suppressed(slot, suppressed_continued_last, cold_store_len); kv_cache_discard_failed_disk_entry(s, slot, disk_cache_path); free(disk_cache_path); if (job_cancelled(j)) { request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled during prefill"); return; } trace_event(s, trace_id, "prefill failed: %s", err); send_prefill_failure_response(s, j, &progress, ctx_span, req_flags, err); return; } if (kv_cache_store_live_prefix(s, slot, prompt_for_sync, cold_store_len, "cold")) { kv_cache_slot_note_store(slot, cold_store_len); suppressed_continued_last = -1; } else { kv_cache_slot_restore_suppressed(slot, suppressed_continued_last, cold_store_len); suppressed_continued_last = -1; } ds4_tokens_free(&prefix); } int prompt_sync_rc = multimodal ? server_session_sync_multimodal(s, slot, prompt_for_sync, j->req.images, j->req.image_count, err, sizeof(err)) : server_session_sync(s, slot, prompt_for_sync, err, sizeof(err)); if (prompt_sync_rc != 0) { ds4_tokens_free(&effective_prompt); ds4_session_set_progress(slot->session, NULL, NULL); ds4_session_set_display_progress(slot->session, NULL, NULL); kv_cache_slot_restore_suppressed(slot, suppressed_continued_last, cold_store_len); kv_cache_discard_failed_disk_entry(s, slot, disk_cache_path); free(disk_cache_path); if (job_cancelled(j)) { request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled during prefill"); return; } trace_event(s, trace_id, "prefill failed: %s", err); send_prefill_failure_response(s, j, &progress, ctx_span, req_flags, err); return; } free(disk_cache_path); if (job_cancelled(j)) { ds4_session_set_progress(slot->session, NULL, NULL); ds4_session_set_display_progress(slot->session, NULL, NULL); request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled after prefill"); ds4_tokens_free(&effective_prompt); return; } /* Once a non-live request wins, old protocol live bindings are stale. Keep * a binding only when this request explicitly continued from it. */ if (!responses_live_continuation) responses_live_clear(s, slot); if (!anthropic_live_continuation) anthropic_live_clear(s, slot); if (!thinking_live_continuation) thinking_live_clear(s, slot); ds4_session_set_progress(slot->session, NULL, NULL); ds4_session_set_display_progress(slot->session, NULL, NULL); if (!multimodal) kv_cache_maybe_store_continued(s, slot); server_log(DS4_LOG_PREFILL, "ds4-server: %s ctx=%s%s%s prompt done %.3fs", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, req_flags[0] ? " " : "", req_flags, now_sec() - t0); if (cold_store_len == prompt_for_sync->len) { if (!multimodal && kv_cache_store_live_prefix(s, slot, prompt_for_sync, cold_store_len, "cold")) { kv_cache_slot_note_store(slot, cold_store_len); suppressed_continued_last = -1; } else { kv_cache_slot_restore_suppressed(slot, suppressed_continued_last, cold_store_len); } } char id[96]; responses_random_id(id, sizeof(id), j->req.kind == REQ_CHAT ? "chatcmpl-" : "cmpl-"); bool structured_stream = request_uses_structured_stream(&j->req); anthropic_stream anthropic_live = {0}; openai_stream openai_live = {0}; responses_stream responses_live = {0}; const bool openai_live_chat = request_uses_openai_live_stream(&j->req); const bool responses_live_chat = request_uses_responses_live_stream(&j->req); long responses_created_at = (long)time(NULL); if (j->req.stream) { if (progress.stream_failed) { server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s%s%s stream closed during prefill", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, req_flags[0] ? " " : "", req_flags); request_live_state_clear(s, slot); ds4_tokens_free(&effective_prompt); return; } /* The prefill progress callback may have already sent the SSE headers * to keep the connection alive during a long prefill. Only emit them * here when prefill never fired (e.g. fully cached prompt). */ if (!progress.headers_sent && !sse_headers(j->fd, s->enable_cors)) { job_mark_cancelled(j); server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s%s%s sse headers failed", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, req_flags[0] ? " " : "", req_flags); request_live_state_clear(s, slot); ds4_tokens_free(&effective_prompt); return; } progress.headers_sent = true; if (j->req.api == API_ANTHROPIC && !anthropic_sse_start_live(j->fd, &j->req, id, prompt_tokens, &anthropic_live)) { job_mark_cancelled(j); server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s anthropic stream start failed", ctx_span); request_live_state_clear(s, slot); ds4_tokens_free(&effective_prompt); return; } if (j->req.api == API_OPENAI && j->req.kind == REQ_CHAT && !sse_chunk(j->fd, &j->req, id, NULL, NULL)) { job_mark_cancelled(j); server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s openai role chunk failed", ctx_span); request_live_state_clear(s, slot); ds4_tokens_free(&effective_prompt); return; } if (openai_live_chat) openai_stream_start(&j->req, &openai_live); if (responses_live_chat) { responses_stream_init(&j->req, &responses_live); responses_live.active = true; if (!responses_sse_created(j->fd, &j->req, &responses_live, responses_created_at)) { job_mark_cancelled(j); server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s%s%s responses created event failed", ctx_span, req_flags[0] ? " " : "", req_flags); responses_stream_free(&responses_live); request_live_state_clear(s, slot); ds4_tokens_free(&effective_prompt); return; } } } bool dsml_recovery_attempted = false; int recovery_completion = 0; uint64_t rng = j->req.seed; if (!rng && !random_bytes(&rng, sizeof(rng))) { rng = ((uint64_t)time(NULL) << 32) ^ (uint64_t)(uintptr_t)j; } if (!rng) rng = UINT64_C(0x9e3779b97f4a7c15); decode_again: ; buf text = {0}; size_t plain_stream_pos = 0; size_t stop_scan_from = 0; const char *finish = "length"; int completion = 0; int room = ds4_session_ctx(slot->session) - ds4_session_pos(slot->session); int max_tokens = server_decode_budget(j->req.max_tokens, recovery_completion, room); bool saw_tool_start = false; bool saw_tool_end = false; bool saw_orphan_tool_end = false; bool client_stop = false; size_t tool_scan_from = 0; int next_tool_progress = 128; int next_decode_log = 50; const char *stop_detail = max_tokens == room ? "context limit" : "output limit"; int stop_token = -1; trace_event(s, trace_id, "prefill done; decode_max=%d ctx_room=%d", max_tokens, room); const double decode_t0 = now_sec(); double last_decode_log_t = decode_t0; int last_decode_log_completion = 0; thinking_state thinking = thinking_state_from_prompt(&j->req); const bool thinking_gates_tool_markers = ds4_think_mode_enabled(j->req.think_mode); bool tool_scan_waiting_for_think_close = thinking_gates_tool_markers && thinking.inside; size_t think_recovery_scan_from = 0; dsml_decode_tracker dsml_tracker; dsml_decode_tracker_init(&dsml_tracker); dsml_tracker.model_syntax = j->req.model_syntax; server_generation_enter(s); while (!g_stop_requested && !job_cancelled(j) && completion < max_tokens && ds4_session_pos(slot->session) < ds4_session_ctx(slot->session)) { dsml_decode_state dsml_state = j->req.kind == REQ_CHAT && j->req.has_tools ? dsml_tracker.decode : DSML_DECODE_OUTSIDE; const bool in_tool_call = dsml_decode_state_is_tool(dsml_state); if (!(j->req.kind == REQ_CHAT && j->req.has_tools && (saw_tool_start || in_tool_call))) { if (!multimodal) kv_cache_maybe_store_continued(s, slot); } float temperature = j->req.temperature; int top_k = j->req.top_k; float top_p = j->req.top_p; float min_p = j->req.min_p; if (ds4_think_mode_enabled(j->req.think_mode)) { /* Thinking keeps the fixed DeepSeek-style sampling defaults, but * only for knobs the client left out: an explicit request value * (e.g. temperature 0 from a benchmark harness) must win, or the * same greedy request returns different text on every call. */ if (!j->req.temperature_set) temperature = DS4_DEFAULT_TEMPERATURE; if (!j->req.top_k_set) top_k = 0; if (!j->req.top_p_set) top_p = DS4_DEFAULT_TOP_P; if (!j->req.min_p_set) min_p = DS4_DEFAULT_MIN_P; } const bool greedy_tool_syntax = !thinking.inside && in_tool_call && !dsml_decode_state_uses_payload_sampling(dsml_state); const float payload_temperature = temperature; if (greedy_tool_syntax) { temperature = 0.0f; } const int eos_token = ds4_token_eos(s->engine); int token = j->req.ignore_eos ? ds4_session_argmax_ignoring_eos(slot->session, j->req.think_mode) : ds4_session_sample(slot->session, temperature, top_k, top_p, min_p, &rng); if (token < 0) { finish = "error"; snprintf(err, sizeof(err), "failed to select a non-EOS token"); break; } if (ds4_token_is_stop_for_think_mode(s->engine, token, j->req.think_mode)) { finish = "stop"; stop_detail = "stop token"; stop_token = token; break; } int toks[17]; int ntok = 0; const int block_start = ds4_session_pos(slot->session); if (!s->batched_mode && ds4_engine_mtp_draft_tokens(s->engine) > 1 && getenv("DS4_MTP_SPEC_DISABLE") == NULL) { if (j->req.ignore_eos) { ntok = ds4_session_eval_speculative_argmax_ignoring_eos( slot->session, token, max_tokens - completion, eos_token, j->req.think_mode, toks, (int)(sizeof(toks) / sizeof(toks[0])), err, sizeof(err)); } else { ntok = ds4_session_eval_speculative( slot->session, token, max_tokens - completion, eos_token, temperature, top_k, top_p, min_p, &rng, toks, (int)(sizeof(toks) / sizeof(toks[0])), err, sizeof(err)); } if (ntok < 0) { finish = "error"; break; } } else if (s->batched_mode && s->qwen4_batch_mtp && max_tokens - completion >= 2 && !j->req.ignore_eos && (!ds4_engine_mtp_exact_sampling(s->engine) || temperature == 0.0f) && getenv("DS4_MTP_SPEC_DISABLE") == NULL) { if (server_eval_tokens(s, slot, token, true, toks, &ntok, err, sizeof(err)) != 0) { finish = "error"; break; } } else { if (server_eval_token(s, slot, token, err, sizeof(err)) != 0) { finish = "error"; break; } toks[0] = token; ntok = 1; } if (ntok == 0) { finish = "error"; snprintf(err, sizeof(err), "decode returned no tokens"); break; } bool stop_decode = false; bool resample = false; bool text_stop = false; int kept = 0; for (int ti = 0; ti < ntok && completion < max_tokens; ti++) { if (job_cancelled(j)) { stop_decode = true; break; } token = toks[ti]; if (ds4_token_is_stop_for_think_mode(s->engine, token, j->req.think_mode)) { finish = "stop"; stop_detail = "stop token"; stop_token = token; stop_decode = true; break; } size_t piece_len = 0; char *piece = ds4_token_text(s->engine, token, &piece_len); completion++; kept++; trace_piece(s, trace_id, piece, piece_len); buf_append(&text, piece, piece_len); const bool was_thinking = thinking.inside; if (!dsml_decode_state_is_tool(dsml_tracker.decode)) thinking_state_feed(&thinking, piece, piece_len); if (j->req.kind == REQ_CHAT && j->req.has_tools) { if (thinking.inside) { dsml_decode_tracker_init(&dsml_tracker); dsml_tracker.model_syntax = j->req.model_syntax; dsml_tracker.pos = text.len; } else { if (was_thinking) { const char *end = find_last_substr(text.ptr, ""); dsml_tracker.pos = end ? (size_t)(end + 8 - text.ptr) : text.len; } dsml_decode_tracker_update(&dsml_tracker, text.ptr, text.len); } } size_t stop_pos = 0, stop_len = 0; bool hit_stop = stop_list_find_from(&j->req.stops, text.ptr, stop_scan_from, &stop_pos, &stop_len); size_t stream_len = hit_stop ? stop_pos : stop_list_stream_safe_len(&j->req.stops, text.len); if (stream_len > text.len) stream_len = text.len; stream_len = utf8_stream_safe_len(text.ptr, plain_stream_pos, stream_len, hit_stop); if (!hit_stop && j->req.stops.max_len > 1) { const size_t hold = j->req.stops.max_len - 1; stop_scan_from = text.len > hold ? text.len - hold : 0; } if (j->req.stream && !structured_stream && stream_len > plain_stream_pos) { char *delta = xstrndup(text.ptr + plain_stream_pos, stream_len - plain_stream_pos); bool ok = sse_chunk(j->fd, &j->req, id, delta, NULL); free(delta); if (!ok) { job_mark_cancelled(j); finish = "error"; snprintf(err, sizeof(err), "client stream write failed"); free(piece); stop_decode = true; break; } plain_stream_pos = stream_len; } if (j->req.stream && j->req.api == API_ANTHROPIC && !anthropic_sse_stream_update(j->fd, s, &j->req, id, &anthropic_live, text.ptr, stream_len, false)) { job_mark_cancelled(j); finish = "error"; snprintf(err, sizeof(err), "client stream write failed"); free(piece); stop_decode = true; break; } if (openai_live_chat && !openai_sse_stream_update(j->fd, s, &j->req, id, &openai_live, text.ptr, stream_len, false)) { job_mark_cancelled(j); finish = "error"; snprintf(err, sizeof(err), "client stream write failed"); free(piece); stop_decode = true; break; } if (responses_live_chat && !responses_sse_stream_update(j->fd, &j->req, &responses_live, text.ptr, stream_len, false)) { job_mark_cancelled(j); finish = "error"; snprintf(err, sizeof(err), "client stream write failed"); free(piece); stop_decode = true; break; } free(piece); if (j->req.kind == REQ_CHAT && j->req.has_tools) { if (thinking_gates_tool_markers && thinking.inside) { /* Do not act on an opening marker alone: it can be quoted * protocol text. A complete block is unambiguous enough to * recover without asking the model to restart it after an * injected close marker. */ if (complete_tool_call_inside_thinking( j->req.model_syntax, text.ptr, text.len, &think_recovery_scan_from)) { saw_tool_start = true; saw_tool_end = true; finish = "tool_calls"; stop_decode = true; server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s recovered a complete tool call from unclosed reasoning after %d generated tokens", ctx_span, req_flags[0] ? " " : "", req_flags, completion); trace_event(s, trace_id, "recovered complete tool call from unclosed reasoning after %d generated tokens", completion); break; } tool_scan_waiting_for_think_close = true; tool_scan_from = text.len; } else { if (tool_scan_waiting_for_think_close) { const char *think_end = find_last_substr(text.ptr, ""); tool_scan_from = think_end ? (size_t)((think_end + 8) - text.ptr) : text.len; if (tool_scan_from > text.len) tool_scan_from = text.len; tool_scan_waiting_for_think_close = false; } if (tool_scan_from > text.len) tool_scan_from = text.len; const char *tool_scan = text.ptr ? text.ptr + tool_scan_from : ""; bool orphan_end = false; bool old_start = saw_tool_start; bool old_end = saw_tool_end; observe_tool_markers(&dsml_tracker, tool_scan, &saw_tool_start, &saw_tool_end, &orphan_end); if (orphan_end && !saw_orphan_tool_end) { saw_orphan_tool_end = true; server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s ignored orphan tool-call end marker after %d generated tokens", ctx_span, req_flags[0] ? " " : "", req_flags, completion); trace_event(s, trace_id, "ignored orphan tool-call end marker after %d generated tokens", completion); } if (saw_tool_start && !old_start) { trace_event(s, trace_id, "entered tool-call block after %d generated tokens", completion); } if (saw_tool_end && !old_end) { trace_event(s, trace_id, "closed tool-call block after %d generated tokens", completion); } const size_t marker_hold = 80; size_t hold_from = text.len > marker_hold ? text.len - marker_hold : 0; if (hold_from > tool_scan_from) tool_scan_from = hold_from; if (s->trace && completion >= next_tool_progress) { trace_event(s, trace_id, "progress gen=%d dsml_start=%d dsml_end=%d", completion, saw_tool_start ? 1 : 0, saw_tool_end ? 1 : 0); next_tool_progress += 128; } } } if (completion >= next_decode_log) { log_decode_progress(j->req.kind, prompt_tokens, completion, responses_protocol, j->req.has_tools, thinking.inside, saw_tool_start, saw_tool_end, decode_t0, &last_decode_log_t, &last_decode_log_completion); next_decode_log += 50; } if (hit_stop) { (void)stop_len; finish = "stop"; stop_detail = "client stop sequence"; client_stop = true; text.len = stop_pos; text.ptr[text.len] = '\0'; pthread_mutex_lock(&s->inference_mu); ds4_session_invalidate(slot->session); pthread_mutex_unlock(&s->inference_mu); text_stop = true; stop_decode = true; break; } if (j->req.kind == REQ_CHAT && j->req.has_tools && saw_tool_end) { finish = "tool_calls"; stop_decode = true; break; } const bool next_greedy = !thinking.inside && dsml_decode_state_is_tool(dsml_tracker.decode) && !dsml_decode_state_uses_payload_sampling(dsml_tracker.decode); /* Opportunistic drafts are greedy under both parser modes. * Only exact sampling changes their acceptance distribution. */ if (ti + 1 < ntok && ds4_engine_mtp_exact_sampling(s->engine) && payload_temperature > 0.0f && next_greedy != greedy_tool_syntax) { resample = true; break; } } if (kept < ntok && !text_stop && !job_cancelled(j) && strcmp(finish, "error")) { /* Logits after a rewind belong to the discarded suffix. Re-eval * the last kept token before sampling under a different mode. */ int pos = block_start + kept - (resample ? 1 : 0); if (server_generation_rewind(s, slot, &j->req, pos, err, sizeof(err)) != 0 || (resample && server_eval_token(s, slot, toks[kept - 1], err, sizeof(err)) != 0)) { finish = "error"; stop_decode = true; } else { trace_event(s, trace_id, "speculative boundary: kept=%d discarded=%d resample=%d", kept, ntok - kept, resample); } } if (stop_decode) break; } server_generation_leave(s); if (job_cancelled(j)) { request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled during generation after %d tokens", completion); anthropic_stream_free(&anthropic_live); openai_stream_free(&openai_live); responses_stream_free(&responses_live); buf_free(&text); ds4_tokens_free(&effective_prompt); return; } if (g_stop_requested && strcmp(finish, "error") != 0) { finish = "error"; snprintf(err, sizeof(err), "shutdown requested"); } if (j->req.kind == REQ_CHAT && j->req.has_tools && saw_tool_start && !saw_tool_end && strcmp(finish, "error") != 0) { server_log(DS4_LOG_WARNING, "ds4-server: incomplete %s tool call: stop=%s token=%d generated=%d limit=%d room=%d", j->req.model_syntax == SERVER_MODEL_SYNTAX_GLM ? "GLM" : "DeepSeek", stop_detail, stop_token, completion, max_tokens, room); trace_event(s, trace_id, "incomplete tool call: stop=%s token=%d generated=%d limit=%d room=%d", stop_detail, stop_token, completion, max_tokens, room); } if (j->req.kind == REQ_CHAT && j->req.has_tools && saw_tool_start && !saw_tool_end && strcmp(finish, "error") != 0 && strcmp(finish, "length") != 0 && !client_stop) { /* Only repair a missing outer wrapper around complete invocations. * Otherwise let the model issue a fresh call within the same output * budget, after a tool error and prompt reminder. */ bool completed_truncation = false; buf repaired = {0}; if (try_repair_dsml(text.ptr, text.len, &repaired)) { /* Parse repaired text to verify it produces valid tool calls */ tool_calls test_calls = {0}; char *test_content = NULL; char *test_reasoning = NULL; bool repair_ok = parse_generated_message_ex_for_syntax( j->req.model_syntax, repaired.ptr, false, &test_content, &test_reasoning, &test_calls); free(test_content); free(test_reasoning); if (repair_ok && test_calls.len > 0) { /* Repair succeeded - replace text with repaired version */ free(text.ptr); text.ptr = buf_take(&repaired); text.len = strlen(text.ptr); saw_tool_end = true; completed_truncation = true; server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s repaired unterminated tool call (%d calls recovered)", ctx_span, req_flags[0] ? " " : "", req_flags, test_calls.len); trace_event(s, trace_id, "repaired unterminated tool call (%d calls recovered)", test_calls.len); } tool_calls_free(&test_calls); } if (!completed_truncation) { if (!j->req.stream && !dsml_recovery_attempted && completion < max_tokens) { int recovery_tokens = 0; char recovery_err[160] = {0}; server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s unterminated tool call; continuing with model-visible tool error", ctx_span, req_flags[0] ? " " : "", req_flags); trace_event(s, trace_id, "unterminated tool call; continuing with model-visible tool error"); if (continue_after_invalid_dsml(s, slot, &j->req, &thinking, "unterminated tool call", &recovery_tokens, recovery_err, sizeof(recovery_err))) { dsml_recovery_attempted = true; recovery_completion += completion; server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s%s%s tool-error continuation appended %d tokens", ctx_span, req_flags[0] ? " " : "", req_flags, recovery_tokens); trace_event(s, trace_id, "tool-error continuation appended %d tokens", recovery_tokens); buf_free(&repaired); buf_free(&text); goto decode_again; } finish = "error"; snprintf(err, sizeof(err), "invalid tool call recovery failed: %s", recovery_err[0] ? recovery_err : "unknown error"); } else { finish = "error"; snprintf(err, sizeof(err), "unterminated tool call (%s; token=%d, generated=%d, limit=%d)", stop_detail, stop_token, completion, max_tokens); } } buf_free(&repaired); } if (completion > last_decode_log_completion) { log_decode_progress(j->req.kind, prompt_tokens, completion, responses_protocol, j->req.has_tools, thinking.inside, saw_tool_start, saw_tool_end, decode_t0, &last_decode_log_t, &last_decode_log_completion); } if (j->req.stream && !structured_stream && text.len > plain_stream_pos) { char *tail = xstrndup(text.ptr + plain_stream_pos, text.len - plain_stream_pos); if (!sse_chunk(j->fd, &j->req, id, tail, NULL)) { job_mark_cancelled(j); finish = "error"; } free(tail); } if (job_cancelled(j)) { request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled while flushing generation"); anthropic_stream_free(&anthropic_live); openai_stream_free(&openai_live); responses_stream_free(&responses_live); buf_free(&text); ds4_tokens_free(&effective_prompt); return; } tool_calls parsed_calls = {0}; char *parsed_content = NULL; char *parsed_reasoning = NULL; const char *final_finish = finish; bool recovered_tool_parse_failure = false; if (j->req.kind == REQ_CHAT) { bool parsed_ok = parse_generated_message_for_response_for_syntax( j->req.model_syntax, text.ptr ? text.ptr : "", j->req.has_tools, saw_tool_start, ds4_think_mode_enabled(j->req.think_mode), &final_finish, err, sizeof(err), &parsed_content, &parsed_reasoning, &parsed_calls, &recovered_tool_parse_failure, &j->req.tool_orders); if (!parsed_ok && recovered_tool_parse_failure && j->req.has_tools && saw_tool_start) { /* parse_generated_message failed even though DSML was present. * Semantic repair is intentionally avoided: if the parser cannot * execute the block, feed the model a tool error and the protocol * reminder so it owns the corrected next action. */ if (!j->req.stream && !dsml_recovery_attempted && strcmp(final_finish, "length") != 0 && !client_stop && completion < max_tokens) { int recovery_tokens = 0; char recovery_err[160] = {0}; const char *detail = err[0] ? err : "invalid tool call"; server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s invalid tool call; continuing with model-visible tool error", ctx_span, req_flags[0] ? " " : "", req_flags); trace_event(s, trace_id, "invalid tool call; continuing with model-visible tool error"); if (continue_after_invalid_dsml(s, slot, &j->req, &thinking, detail, &recovery_tokens, recovery_err, sizeof(recovery_err))) { dsml_recovery_attempted = true; recovery_completion += completion; server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s%s%s tool-error continuation appended %d tokens", ctx_span, req_flags[0] ? " " : "", req_flags, recovery_tokens); trace_event(s, trace_id, "tool-error continuation appended %d tokens", recovery_tokens); free(parsed_content); free(parsed_reasoning); tool_calls_free(&parsed_calls); buf_free(&text); goto decode_again; } final_finish = "error"; snprintf(err, sizeof(err), "invalid tool call recovery failed: %s", recovery_err[0] ? recovery_err : "unknown error"); } if (!parsed_ok) { /* Print raw DSML snippet for debugging */ size_t dsml_snippet_len = 0; const char *dsml_start = find_any_tool_start(text.ptr ? text.ptr : ""); if (dsml_start) { dsml_snippet_len = text.len - (dsml_start - text.ptr); if (dsml_snippet_len > 500) dsml_snippet_len = 500; } /* Also log a snippet of the full text to see what the model output */ size_t text_snippet_len = text.len > 300 ? 300 : text.len; server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s invalid tool call returned as assistant text finish=%s [text_len=%zu saw_start=%d saw_end=%d text_snippet: %.*s]", ctx_span, req_flags[0] ? " " : "", req_flags, final_finish, text.len, saw_tool_start, saw_tool_end, (int)text_snippet_len, text.ptr ? text.ptr : "(null)"); server_log(DS4_LOG_WARNING, "ds4-server: chat ctx=%s%s%s invalid tool call dsml_snippet: %.*s", ctx_span, req_flags[0] ? " " : "", req_flags, (int)dsml_snippet_len, dsml_start ? dsml_start : "(none)"); trace_event(s, trace_id, "invalid tool call returned as assistant text finish=%s", final_finish); } } if (job_cancelled(j)) { request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled during response parsing"); free(parsed_content); free(parsed_reasoning); tool_calls_free(&parsed_calls); anthropic_stream_free(&anthropic_live); openai_stream_free(&openai_live); responses_stream_free(&responses_live); buf_free(&text); ds4_tokens_free(&effective_prompt); return; } if (parsed_calls.len) { if (openai_live_chat) apply_openai_stream_tool_ids(&parsed_calls, &openai_live); if (j->req.api == API_ANTHROPIC && j->req.stream) apply_anthropic_stream_tool_ids(&parsed_calls, &anthropic_live); assign_tool_call_ids(s, &parsed_calls, j->req.api); tool_memory_remember(s, &parsed_calls); final_finish = "tool_calls"; } else if (j->req.api == API_RESPONSES) { responses_live_clear(s, slot); } } if (job_cancelled(j)) { request_live_state_clear(s, slot); trace_event(s, trace_id, "cancelled before publishing response state"); free(parsed_content); free(parsed_reasoning); tool_calls_free(&parsed_calls); anthropic_stream_free(&anthropic_live); openai_stream_free(&openai_live); responses_stream_free(&responses_live); buf_free(&text); ds4_tokens_free(&effective_prompt); return; } log_tool_calls_summary(ctx_span, &parsed_calls, responses_protocol); completion += recovery_completion; trace_finish(s, trace_id, &j->req, final_finish, completion, saw_tool_start, saw_tool_end, parsed_content ? parsed_content : (text.ptr ? text.ptr : ""), parsed_reasoning, &parsed_calls, now_sec() - t0); if (j->req.api == API_RESPONSES) { if (strcmp(final_finish, "error") && strcmp(final_finish, "length")) { /* Store the post-turn visible transcript plus the live token * frontier. The next Responses request may replay only this * visible surface, while the real session also contains hidden * reasoning and exact sampled tool-call bytes. */ char *visible_suffix = build_responses_visible_assistant_suffix(&j->req, parsed_content ? parsed_content : "", parsed_reasoning, &parsed_calls); buf visible = {0}; buf_puts(&visible, j->req.prompt_text ? j->req.prompt_text : ""); buf_puts(&visible, visible_suffix ? visible_suffix : ""); responses_live_remember(s, slot, visible.ptr ? visible.ptr : "", parsed_calls.len ? &parsed_calls : NULL, &j->req); buf_free(&visible); free(visible_suffix); } else { responses_live_clear(s, slot); } } if (j->req.api == API_ANTHROPIC) { if (parsed_calls.len && strcmp(final_finish, "error") && strcmp(final_finish, "length")) { anthropic_live_remember(s, slot, &parsed_calls); } else { anthropic_live_clear(s, slot); } } if (j->req.kind == REQ_CHAT && parsed_calls.len && j->req.api != API_RESPONSES && should_canonicalize_tool_checkpoint(s, &parsed_calls)) { /* Chat/completions has no protocol object that binds the next request * to this live KV state. Canonicalize only the fallback tool-call * path where we lack exact sampled DSML replay; when raw DSML is known, * replaying those bytes keeps future prompts aligned without rebuilding * hidden reasoning. Responses deliberately skips this path because its * previous_response_id contract binds the next turn to live state. */ canonicalize_tool_checkpoint(s, slot, j, ctx_span, trace_id, parsed_content ? parsed_content : "", parsed_reasoning, &parsed_calls); thinking_live_clear(s, slot); } else if (parsed_calls.len) { if (!remember_qwen_tool_turn_visible_checkpoint( s, slot, j, ctx_span, finish, thinking.inside, parsed_content ? parsed_content : "", &parsed_calls)) { thinking_live_clear(s, slot); } } else if (!parsed_calls.len && should_remember_thinking_checkpoint(&j->req, &thinking, final_finish)) { remember_thinking_checkpoint(s, slot, j, ctx_span, trace_id, parsed_content ? parsed_content : "", thinking_live_continuation || (disk_cache_ext_flags & KV_EXT_THINKING_VISIBLE)); } else if (!parsed_calls.len) { thinking_live_clear(s, slot); } bool response_ok = !job_cancelled(j); if (response_ok && j->req.stream) { if (j->req.api == API_ANTHROPIC) { response_ok = anthropic_sse_finish_live(j->fd, s, &j->req, id, &anthropic_live, text.ptr ? text.ptr : "", text.len, &parsed_calls, final_finish, completion); } else if (openai_live_chat) { response_ok = openai_sse_finish_live(j->fd, s, &j->req, id, &openai_live, text.ptr ? text.ptr : "", text.len, &parsed_calls, final_finish, prompt_tokens, completion); } else if (responses_live_chat) { /* If parse recovered a malformed tool call back to plain text, * pass parsed_content so the streaming tail can be flushed; in * the normal path parsed_content is the assistant text we already * streamed and the diff is empty. */ const char *recover = recovered_tool_parse_failure ? parsed_content : NULL; response_ok = responses_sse_finish_live(j->fd, &j->req, &responses_live, text.ptr ? text.ptr : "", text.len, recover, &parsed_calls, final_finish, prompt_tokens, completion, responses_created_at); } else if (structured_stream) { response_ok = sse_chat_finish(j->fd, &j->req, id, parsed_content ? parsed_content : (text.ptr ? text.ptr : ""), parsed_reasoning, &parsed_calls, final_finish, prompt_tokens, completion); } else { response_ok = sse_chunk(j->fd, &j->req, id, NULL, final_finish) && sse_done(j->fd, &j->req, id, prompt_tokens, completion); } } else if (response_ok && j->req.api == API_ANTHROPIC) { response_ok = anthropic_final_response(j->fd, s->enable_cors, &j->req, id, parsed_content ? parsed_content : (text.ptr ? text.ptr : ""), parsed_reasoning, &parsed_calls, final_finish, prompt_tokens, completion); } else if (response_ok && j->req.api == API_RESPONSES) { response_ok = responses_final_response(j->fd, s->enable_cors, &j->req, id, parsed_content ? parsed_content : (text.ptr ? text.ptr : ""), parsed_reasoning, &parsed_calls, final_finish, prompt_tokens, completion); } else if (response_ok) { response_ok = final_response(j->fd, s->enable_cors, &j->req, id, parsed_content ? parsed_content : (text.ptr ? text.ptr : ""), parsed_reasoning, &parsed_calls, final_finish, prompt_tokens, completion); } if (job_cancelled(j)) response_ok = false; if (!response_ok) { job_mark_cancelled(j); final_finish = "error"; snprintf(err, sizeof(err), "client disconnected"); request_live_state_clear(s, slot); server_log(DS4_LOG_DEFAULT, "ds4-server: %s ctx=%s%s%s client disconnected", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, req_flags[0] ? " " : "", req_flags); } if (j->req.kind == REQ_CHAT && j->req.has_tools) { char flags[80]; log_flags(flags, sizeof(flags), responses_protocol, true, thinking.inside, saw_tool_start, saw_tool_end); if (!strcmp(final_finish, "error") && err[0]) { server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s gen=%d%s%s finish=%s error=\"%s\" %.3fs", ctx_span, completion, flags[0] ? " " : "", flags, final_finish, err, now_sec() - t0); } else { server_log(DS4_LOG_GENERATION, "ds4-server: chat ctx=%s gen=%d%s%s finish=%s %.3fs", ctx_span, completion, flags[0] ? " " : "", flags, final_finish, now_sec() - t0); } } else { char flags[80]; log_flags(flags, sizeof(flags), responses_protocol, j->req.has_tools, thinking.inside, false, false); if (!strcmp(final_finish, "error") && err[0]) { server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s gen=%d%s%s finish=%s error=\"%s\" %.3fs", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, completion, flags[0] ? " " : "", flags, final_finish, err, now_sec() - t0); } else { server_log(DS4_LOG_GENERATION, "ds4-server: %s ctx=%s gen=%d%s%s finish=%s %.3fs", j->req.kind == REQ_CHAT ? "chat" : "completion", ctx_span, completion, flags[0] ? " " : "", flags, final_finish, now_sec() - t0); } } free(parsed_content); free(parsed_reasoning); tool_calls_free(&parsed_calls); anthropic_stream_free(&anthropic_live); openai_stream_free(&openai_live); responses_stream_free(&responses_live); buf_free(&text); ds4_tokens_free(&effective_prompt); } /* Keep cancellation installed for the entire request, including every early * return in the large protocol/generation path. The callback is cleared before * the client thread can destroy its stack-owned job. */ static void generate_job(server *s, server_slot *slot, job *j) { pthread_mutex_lock(&s->model_mu); slot->running = j; pthread_mutex_unlock(&s->model_mu); ds4_session_set_cancel(slot->session, job_cancelled, j); if (!job_cancelled(j)) generate_job_inner(s, slot, j); ds4_session_set_cancel(slot->session, NULL, NULL); pthread_mutex_lock(&s->model_mu); if (slot->running == j) slot->running = NULL; pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); } static bool live_state_contains_all(const live_tool_state *state, const stop_list *ids) { if (!state || !state->valid || !ids || ids->len == 0) return false; for (int i = 0; i < ids->len; i++) { if (!id_list_contains(&state->call_ids, ids->v[i])) return false; } return true; } /* Return the only slot eligible for an explicit live continuation, or -1 when * the request has no resident binding. A missing binding is intentionally not * treated as ineligible: generate_job() then emits the existing 409 response. */ static int job_required_slot_locked(server *s, const job *j) { if (!s || !j) return -1; const request *r = &j->req; for (int i = 0; i < s->slot_count; i++) { server_slot *slot = &s->slots[i]; if (r->responses_requires_live_tool_state && live_state_contains_all(&slot->responses_live, &r->responses_live_call_ids)) { return i; } if (r->anthropic_requires_live_tool_state && live_state_contains_all(&slot->anthropic_live, &r->anthropic_live_call_ids)) { return i; } } return -1; } /* A resident checkpoint idle longer than this is most likely a finished * conversation: it becomes the preferred eviction victim regardless of its * size (see job_slot_score). Without this, long-forgotten sessions are * never evicted (evicting them looks expensive), so they pile up on a * long-running server until only short *active* conversations remain as * victims - and prefill thrashing returns. */ #define SLOT_STALE_AFTER_SEC ((time_t)60 * 60) /* Score layout for job_slot_score()'s no-reuse tiers (all negative; every * score of a lower tier must stay strictly below every score of the tier * above): * empty slot: 0 * stale checkpoint: [-1 - SLOT_IDLE_SCORE_CAP + SLOT_STALE_AFTER_SEC, -1] * (more idle = better victim) * protected checkpoint: SLOT_PROTECTED_SCORE_BASE - live * (shorter = better victim) */ #define SLOT_IDLE_SCORE_CAP (1 << 20) /* ~12 days; keeps the tiers apart */ #define SLOT_PROTECTED_SCORE_BASE (-2 - SLOT_IDLE_SCORE_CAP) static int job_slot_score(server *s, server_slot *slot, const job *j, int required_slot, time_t now) { if (!s || !slot || !j || slot->busy || slot->assigned) return INT_MIN; if (required_slot >= 0 && slot->id != required_slot) return INT_MIN; if (required_slot == slot->id) return INT_MAX; /* Reuse-aware routing: a slot is attractive only when this request would * actually reuse its live state - the same verdict the execution ladder * reaches via slot_probe_reuse_locked() (dispatch holds tool_mu here). * The score is the reusable prefix length, so the slot where more of the * prompt is already resident wins, and any reuse beats an empty slot. * * A request that can reuse nothing must evict a resident checkpoint, so * it goes to the cheapest victim. "Cheapest" is staleness-aware: a * checkpoint idle past SLOT_STALE_AFTER_SEC is most likely a finished * conversation, so the more idle it is the better a victim it makes, * regardless of size; a fresher checkpoint belongs to an active * conversation, so the shortest one is evicted, losing the least * prefilled work. Staleness only lowers a checkpoint's priority as a * *victim* - it never blocks reuse: if the owner of a stale checkpoint * continues its conversation, the reuse tier above still pins the slot. */ const int live = slot->session && ds4_session_checkpoint_valid(slot->session) ? ds4_session_pos(slot->session) : 0; const int reuse = slot_probe_reuse_locked(s, slot, &j->req).reuse_tokens; if (reuse > 0) return reuse; if (live == 0) return 0; const time_t idle = now > slot->last_used ? now - slot->last_used : 0; if (idle >= SLOT_STALE_AFTER_SEC) { const time_t capped = idle > SLOT_IDLE_SCORE_CAP ? SLOT_IDLE_SCORE_CAP : idle; return -1 - SLOT_IDLE_SCORE_CAP + (int)capped; } return SLOT_PROTECTED_SCORE_BASE - live; } static void dispatch_jobs_locked(server *s) { if (!s || !s->batched_mode) return; const time_t now = time(NULL); for (;;) { job *chosen = NULL; job *chosen_prev = NULL; server_slot *chosen_slot = NULL; int chosen_score = INT_MIN; int chosen_evict_live = 0; time_t chosen_evict_idle = 0; pthread_mutex_lock(&s->tool_mu); job *prev = NULL; for (job *j = s->head; j; prev = j, j = j->next) { int required = job_required_slot_locked(s, j); server_slot *best = NULL; int best_score = INT_MIN; for (int i = 0; i < s->slot_count; i++) { int score = job_slot_score(s, &s->slots[i], j, required, now); if (score > best_score) { best_score = score; best = &s->slots[i]; } } if (best) { chosen = j; chosen_prev = prev; chosen_slot = best; chosen_score = best_score; break; /* FIFO among jobs that can run now. */ } } /* chosen_score < 0 means the request reuses nothing and is routed * onto a resident checkpoint, evicting it: capture what is lost so * the eviction is directly visible in the log. */ if (chosen && chosen_score < 0 && chosen_slot->session && ds4_session_checkpoint_valid(chosen_slot->session)) { chosen_evict_live = ds4_session_pos(chosen_slot->session); chosen_evict_idle = now > chosen_slot->last_used ? now - chosen_slot->last_used : 0; } pthread_mutex_unlock(&s->tool_mu); if (!chosen || !chosen_slot) break; if (chosen_prev) chosen_prev->next = chosen->next; else s->head = chosen->next; if (s->tail == chosen) s->tail = chosen_prev; chosen->next = NULL; chosen_slot->assigned = chosen; chosen_slot->busy = true; if (chosen_evict_live > 0) { server_log(DS4_LOG_KVCACHE, "slot %d: request reuses nothing; evicting checkpoint " "(%d tokens, idle %lld s, %s)", chosen_slot->id, chosen_evict_live, (long long)chosen_evict_idle, chosen_evict_idle >= SLOT_STALE_AFTER_SEC ? "stale" : "protected"); } pthread_cond_broadcast(&s->cv); } } static bool enqueue(server *s, job *j) { pthread_mutex_lock(&s->mu); if (s->stopping) { pthread_mutex_unlock(&s->mu); return false; } if (s->tail) s->tail->next = j; else s->head = j; s->tail = j; if (s->batched_mode) { dispatch_jobs_locked(s); pthread_cond_broadcast(&s->cv); } else { pthread_cond_signal(&s->cv); } pthread_mutex_unlock(&s->mu); return true; } static job *dequeue(server *s) { pthread_mutex_lock(&s->mu); while (!s->head && !s->stopping) pthread_cond_wait(&s->cv, &s->mu); if (!s->head) { pthread_mutex_unlock(&s->mu); return NULL; } job *j = s->head; s->head = j->next; if (!s->head) s->tail = NULL; pthread_mutex_unlock(&s->mu); j->next = NULL; return j; } static void *worker_main(void *arg) { server *s = arg; for (;;) { job *j = dequeue(s); if (!j) break; generate_job(s, &s->slots[0], j); job_complete(j); /* Mirror slot_worker_main: the checkpoint may have changed in any way * during the job, and the memory-text probe tier plus the staleness * tiers read the slot's rendered-text cache and activity stamp. */ slot_refresh_live_text(s, &s->slots[0]); s->slots[0].last_used = time(NULL); } return NULL; } static void *slot_worker_main(void *arg) { server_slot *slot = arg; server *s = slot->srv; for (;;) { pthread_mutex_lock(&s->mu); while (!slot->assigned && (!s->stopping || s->head)) { pthread_cond_wait(&s->cv, &s->mu); } if (!slot->assigned && s->stopping && !s->head) { pthread_mutex_unlock(&s->mu); break; } job *j = slot->assigned; slot->assigned = NULL; pthread_mutex_unlock(&s->mu); generate_job(s, slot, j); job_complete(j); /* The checkpoint may have changed in any way during the job (sync, * rewind, generation, disk load, reset); refresh the rendered-text * cache the memory-text probe relies on before the slot becomes * visible to the router as free again. Also stamp the activity * time the router's staleness tiers rely on. */ slot_refresh_live_text(s, slot); slot->last_used = time(NULL); pthread_mutex_lock(&s->mu); slot->busy = false; dispatch_jobs_locked(s); pthread_mutex_unlock(&s->mu); } return NULL; } typedef struct { char method[8]; char path[256]; char *body; size_t body_len; } http_request; static void http_request_free(http_request *r) { free(r->body); memset(r, 0, sizeof(*r)); } static ssize_t header_end(const char *p, size_t n) { for (size_t i = 3; i < n; i++) { if (p[i - 3] == '\r' && p[i - 2] == '\n' && p[i - 1] == '\r' && p[i] == '\n') return (ssize_t)(i + 1); } for (size_t i = 1; i < n; i++) { if (p[i - 1] == '\n' && p[i] == '\n') return (ssize_t)(i + 1); } return -1; } static long content_length(const char *h, size_t n) { const char *p = h, *end = h + n; while (p < end) { const char *line = p; while (p < end && *p != '\n') p++; size_t len = (size_t)(p - line); if (len && line[len - 1] == '\r') len--; if (len >= 15 && strncasecmp(line, "Content-Length:", 15) == 0) { const char *v = line + 15; while (v < line + len && isspace((unsigned char)*v)) v++; return strtol(v, NULL, 10); } if (p < end) p++; } return 0; } static bool read_http_request(int fd, http_request *r) { buf b = {0}; ssize_t hend = -1; const size_t max_header = 64 * 1024; const size_t max_body = 64 * 1024 * 1024; while (hend < 0 && b.len < max_header) { char tmp[4096]; ssize_t n = recv(fd, tmp, sizeof(tmp), 0); if (n < 0 && errno == EINTR) continue; if (n <= 0) goto fail; buf_append(&b, tmp, (size_t)n); hend = header_end(b.ptr, b.len); } if (hend < 0) goto fail; char line[512]; size_t i = 0; while (i < b.len && b.ptr[i] != '\n' && i + 1 < sizeof(line)) { line[i] = b.ptr[i]; i++; } line[i] = '\0'; if (sscanf(line, "%7s %255s", r->method, r->path) != 2) goto fail; char *q = strchr(r->path, '?'); if (q) *q = '\0'; long clen = content_length(b.ptr, (size_t)hend); if (clen < 0 || (size_t)clen > max_body) goto fail; while (b.len < (size_t)hend + (size_t)clen) { char tmp[8192]; ssize_t n = recv(fd, tmp, sizeof(tmp), 0); if (n < 0 && errno == EINTR) continue; if (n <= 0) goto fail; buf_append(&b, tmp, (size_t)n); } r->body_len = (size_t)clen; r->body = xmalloc(r->body_len + 1); memcpy(r->body, b.ptr + hend, r->body_len); r->body[r->body_len] = '\0'; buf_free(&b); return true; fail: buf_free(&b); return false; } typedef struct { server *srv; int fd; } client_arg; static void append_model_json_values(buf *b, const char *id, const char *name, int ctx, int default_tokens) { const int max_completion = default_tokens < ctx ? default_tokens : ctx; buf_printf(b, "{\"id\":"); json_escape(b, id); buf_puts(b, ",\"object\":\"model\"," "\"created\":1767225600," "\"owned_by\":\"ds4.c\"," "\"name\":"); json_escape(b, name); buf_printf(b, "," "\"context_length\":%d," "\"top_provider\":{" "\"context_length\":%d," "\"max_completion_tokens\":%d," "\"is_moderated\":false}," "\"supported_parameters\":[" "\"tools\"," "\"tool_choice\"," "\"max_tokens\"," "\"temperature\"," "\"top_p\"," "\"top_k\"," "\"min_p\"," "\"ignore_eos\"," "\"stop\"," "\"seed\"," "\"stream\"," "\"reasoning_effort\"]}", ctx, ctx, max_completion); } static void append_model_json(buf *b, const server *s, const char *id) { append_model_json_values(b, id, ds4_engine_model_name(s->engine), s->ctx_size, s->default_tokens); } static bool send_model(server *s, int fd, const char *id) { buf b = {0}; append_model_json(&b, s, id); buf_putc(&b, '\n'); bool ok = http_response(fd, s->enable_cors, 200, "application/json", b.ptr); buf_free(&b); return ok; } static bool send_models(server *s, int fd) { buf b = {0}; buf_puts(&b, "{\"object\":\"list\",\"data\":["); if (ds4_engine_is_deepseek41(s->engine)) { append_model_json(&b, s, server_model_id_from_engine(s->engine)); } else if (ds4_engine_is_qwen4(s->engine)) { append_model_json(&b, s, "qwen3.8-flash-next"); buf_putc(&b, ','); append_model_json(&b, s, "qwen3.8-flash-next-chat"); buf_putc(&b, ','); append_model_json(&b, s, "qwen3.8-flash-next-reasoner"); } else if (ds4_engine_is_glm_dsa(s->engine)) { const char *base = server_model_id_from_engine(s->engine); char variant[64]; append_model_json(&b, s, base); buf_putc(&b, ','); snprintf(variant, sizeof(variant), "%s-chat", base); append_model_json(&b, s, variant); buf_putc(&b, ','); snprintf(variant, sizeof(variant), "%s-reasoner", base); append_model_json(&b, s, variant); } else { append_model_json(&b, s, "deepseek-v4-flash"); buf_putc(&b, ','); append_model_json(&b, s, "deepseek-v4-pro"); } buf_puts(&b, "]}\n"); bool ok = http_response(fd, s->enable_cors, 200, "application/json", b.ptr); buf_free(&b); return ok; } static void client_done(server *s) { pthread_mutex_lock(&s->mu); if (s->clients > 0) s->clients--; pthread_cond_broadcast(&s->clients_cv); pthread_mutex_unlock(&s->mu); } static void set_client_socket_nonblocking(int fd); static bool client_poll_revents_disconnected(short revents) { return (revents & (POLLERR | POLLHUP | POLLNVAL)) != 0; } static bool client_recv_errno_disconnected(int err) { return err != EINTR && err != EAGAIN && err != EWOULDBLOCK; } /* The request body has already been consumed and this one-request server sends * Connection: close, so EOF is cancellation. Discard unsupported pipelined * bytes nonblockingly: otherwise they can hide the FIN behind readable data. */ static bool client_socket_disconnected(int fd) { struct pollfd pfd = {.fd = fd, .events = POLLIN}; int rc; do { rc = poll(&pfd, 1, 0); } while (rc < 0 && errno == EINTR); if (rc < 0) return client_recv_errno_disconnected(errno); if (rc == 0) return false; if (client_poll_revents_disconnected(pfd.revents)) return true; if (!(pfd.revents & POLLIN)) return false; char discard[256]; for (;;) { ssize_t n = recv(fd, discard, sizeof(discard), 0); if (n > 0) continue; if (n == 0) return true; if (errno == EINTR) continue; return client_recv_errno_disconnected(errno); } } /* Mark first, then detach only work that no worker owns yet. No job mutex is * held while entering either server scheduler mutex. */ static void server_cancel_job(server *s, job *j) { job_mark_cancelled(j); bool detached = false; pthread_mutex_lock(&s->mu); job *prev = NULL; for (job *it = s->head; it; prev = it, it = it->next) { if (it != j) continue; if (prev) prev->next = it->next; else s->head = it->next; if (s->tail == it) s->tail = prev; it->next = NULL; detached = true; break; } if (!detached && s->batched_mode) { for (int i = 0; i < s->slot_count; i++) { server_slot *slot = &s->slots[i]; if (slot->assigned != j) continue; slot->assigned = NULL; slot->busy = false; detached = true; dispatch_jobs_locked(s); break; } } pthread_cond_broadcast(&s->cv); pthread_mutex_unlock(&s->mu); pthread_mutex_lock(&s->model_mu); for (int i = 0; i < s->slot_count; i++) { server_slot *slot = &s->slots[i]; if (slot->running == j) { (void)server_cancel_pending_decode_locked(s, slot); break; } } pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); if (detached) job_complete(j); } static void wait_for_job_or_disconnect(server *s, job *j) { pthread_mutex_lock(&j->mu); while (!j->done) { struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); timespec_add_us(&deadline, 100000); (void)pthread_cond_timedwait(&j->cv, &j->mu, &deadline); bool done = j->done; pthread_mutex_unlock(&j->mu); if (!done && client_socket_disconnected(j->fd)) { server_cancel_job(s, j); } pthread_mutex_lock(&j->mu); } pthread_mutex_unlock(&j->mu); } static void *client_main(void *arg) { client_arg *ca = arg; server *s = ca->srv; int fd = ca->fd; free(ca); http_request hr = {0}; if (!read_http_request(fd, &hr)) { http_error(fd, s->enable_cors, 400, "bad HTTP request"); goto done; } if (!strcmp(hr.method, "OPTIONS")) { http_response(fd, s->enable_cors, 204, NULL, ""); http_request_free(&hr); goto done; } if (!strcmp(hr.method, "GET") && !strcmp(hr.path, "/v1/models")) { send_models(s, fd); http_request_free(&hr); goto done; } const char *model_path_prefix = "/v1/models/"; const size_t model_path_prefix_len = strlen(model_path_prefix); if (!strcmp(hr.method, "GET") && !strncmp(hr.path, model_path_prefix, model_path_prefix_len) && server_model_alias_known(hr.path + model_path_prefix_len)) { send_model(s, fd, hr.path + model_path_prefix_len); http_request_free(&hr); goto done; } request req; char err[160]; bool ok = false; const int ctx_size = s->ctx_size; if (!strcmp(hr.method, "POST") && !strcmp(hr.path, "/v1/messages")) { ok = parse_anthropic_request(s->engine, s, hr.body, s->default_tokens, ctx_size, &req, err, sizeof(err)); } else if (!strcmp(hr.method, "POST") && !strcmp(hr.path, "/v1/chat/completions")) { ok = parse_chat_request(s->engine, s, hr.body, s->default_tokens, ctx_size, &req, err, sizeof(err)); } else if (!strcmp(hr.method, "POST") && !strcmp(hr.path, "/v1/responses")) { ok = parse_responses_request(s->engine, s, hr.body, s->default_tokens, ctx_size, &req, err, sizeof(err)); } else if (!strcmp(hr.method, "POST") && !strcmp(hr.path, "/v1/completions")) { ok = parse_completion_request(s->engine, hr.body, s->default_tokens, ctx_size, &req, err, sizeof(err)); } else { http_error(fd, s->enable_cors, 404, "unknown endpoint"); http_request_free(&hr); goto done; } if (ok) req.raw_body = xstrndup(hr.body, hr.body_len); http_request_free(&hr); if (!ok) { http_error(fd, s->enable_cors, 400, err); goto done; } if (!req.model_from_request) { free(req.model); req.model = xstrdup(server_model_id_from_engine(s->engine)); } if (request_exceeds_context(&req, ctx_size)) { http_error_context_length_exceeded(fd, s->enable_cors, &req, req.prompt.len, ctx_size); request_free(&req); goto done; } set_client_socket_nonblocking(fd); job j; memset(&j, 0, sizeof(j)); j.fd = fd; j.req = req; pthread_mutex_init(&j.mu, NULL); pthread_cond_init(&j.cv, NULL); if (!enqueue(s, &j)) { http_error(fd, s->enable_cors, 503, "server shutting down"); pthread_cond_destroy(&j.cv); pthread_mutex_destroy(&j.mu); request_free(&j.req); goto done; } wait_for_job_or_disconnect(s, &j); pthread_cond_destroy(&j.cv); pthread_mutex_destroy(&j.mu); request_free(&j.req); done: close(fd); client_done(s); return NULL; } static int listen_on(const char *host, int port) { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) return -1; int yes = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons((uint16_t)port); if (!strcmp(host, "localhost")) host = "127.0.0.1"; if (inet_pton(AF_INET, host, &sa.sin_addr) != 1) { close(fd); errno = EINVAL; return -1; } if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) != 0) { close(fd); return -1; } if (listen(fd, 128) != 0) { close(fd); return -1; } return fd; } static void configure_client_socket(int fd) { struct timeval tv; tv.tv_sec = DS4_SERVER_IO_TIMEOUT_SEC; tv.tv_usec = 0; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); } static void set_client_socket_nonblocking(int fd) { /* The inference worker writes streaming responses itself. Once a request is * queued, a blocked socket would block every other request too, so slow * clients are failed instead of back-pressuring the model session. */ int flags = fcntl(fd, F_GETFL, 0); if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK); } typedef struct { ds4_engine_options engine; const char *gpu_vram_arg; const char *gpu_devices_arg; const char *host; int port; int ctx_size; int default_tokens; const char *chdir_path; const char *trace_path; const char *kv_disk_dir; uint64_t kv_disk_space_mb; kv_cache_options kv_cache; bool kv_cache_reject_different_quant; bool disable_exact_dsml_tool_replay; int tool_memory_max_ids; bool enable_cors; int batched_sessions; int mixed_prefill_quantum; } server_config; static int parse_int_arg(const char *s, const char *opt) { char *end = NULL; long v = strtol(s, &end, 10); if (!s[0] || *end || v <= 0 || v > INT_MAX) { server_log(DS4_LOG_DEFAULT, "ds4-server: invalid value for %s: %s", opt, s); exit(2); } return (int)v; } static int parse_nonneg_int_arg(const char *s, const char *opt) { char *end = NULL; long v = strtol(s, &end, 10); if (!s[0] || *end || v < 0 || v > INT_MAX) { server_log(DS4_LOG_DEFAULT, "ds4-server: invalid value for %s: %s", opt, s); exit(2); } return (int)v; } static float parse_float_arg(const char *s, const char *opt, float minv, float maxv) { char *end = NULL; float v = strtof(s, &end); if (!s[0] || *end || v < minv || v > maxv) { server_log(DS4_LOG_DEFAULT, "ds4-server: invalid value for %s: %s", opt, s); exit(2); } return v; } static const char *need_arg(int *i, int argc, char **argv, const char *opt) { if (*i + 1 >= argc) { server_log(DS4_LOG_DEFAULT, "ds4-server: missing value for %s", opt); exit(2); } return argv[++(*i)]; } static void log_context_memory(ds4_backend backend, int ctx_size, uint32_t prefill_chunk, bool ssd_streaming, int session_count) { ds4_context_memory m = ds4_context_memory_estimate_with_prefill_mode(backend, ctx_size, prefill_chunk, ssd_streaming); server_log(DS4_LOG_DEFAULT, "ds4-server: context buffers %.2f MiB (ctx=%d, backend=%s, prefill_chunk=%u, raw_kv_rows=%u, compressed_kv_rows=%u)", (double)m.total_bytes / (1024.0 * 1024.0), ctx_size, ds4_backend_name(backend), m.prefill_cap, m.raw_cap, m.comp_cap); if (session_count > 1) { /* Only the caches repeat per slot. Where the model shares one prefill * workspace across slots, the transients are paid once instead. */ const double gib = 1024.0 * 1024.0 * 1024.0; const double caches = (double)(m.raw_bytes + m.compressed_bytes); server_log(DS4_LOG_DEFAULT, "ds4-server: %d resident sessions request at least %.2f GiB of " "context buffers, or %.2f GiB when the model shares one prefill " "workspace", session_count, (double)m.total_bytes * (double)session_count / gib, (caches * (double)session_count + (double)m.scratch_bytes) / gib); } } static void server_close_resources(server *s) { if (s->trace) { fclose(s->trace); s->trace = NULL; } kv_cache_close(&s->kv); tool_memory_free(&s->tool_mem); server_image_cache_clear(&s->image_cache); for (int i = 0; i < s->slot_count; i++) { server_slot *slot = &s->slots[i]; live_tool_state_free(&slot->responses_live); live_tool_state_free(&slot->anthropic_live); visible_live_free(&slot->thinking_live); free(slot->live_text); if (slot->session) ds4_session_free(slot->session); } free(s->slot_threads); free(s->slots); pthread_mutex_destroy(&s->tool_mu); pthread_mutex_destroy(&s->kv_mu); pthread_mutex_destroy(&s->inference_mu); pthread_mutex_destroy(&s->model_mu); pthread_mutex_destroy(&s->trace_mu); pthread_cond_destroy(&s->model_cv); pthread_cond_destroy(&s->clients_cv); pthread_cond_destroy(&s->cv); pthread_mutex_destroy(&s->mu); if (s->tp_leader) ds4_tp_send_stop(s->tp_leader); ds4_engine_close(s->engine); ds4_tp_free(s->tp_leader); memset(s, 0, sizeof(*s)); } static void usage(FILE *fp, const char *topic) { ds4_help_print(fp, DS4_HELP_SERVER, topic); } static ds4_backend parse_backend_arg(const char *s, const char *arg) { if (!strcmp(s, "metal")) return DS4_BACKEND_METAL; #ifdef DS4_ROCM_BUILD if (!strcmp(s, "rocm")) return DS4_BACKEND_CUDA; #else if (!strcmp(s, "cuda")) return DS4_BACKEND_CUDA; #endif if (!strcmp(s, "cpu")) return DS4_BACKEND_CPU; server_log(DS4_LOG_DEFAULT, "ds4-server: invalid %s value: %s", arg, s); #ifdef DS4_ROCM_BUILD server_log(DS4_LOG_DEFAULT, "ds4-server: valid server backends are: metal, rocm, cpu"); #else server_log(DS4_LOG_DEFAULT, "ds4-server: valid server backends are: metal, cuda, cpu"); #endif exit(2); } static ds4_backend default_server_backend(void) { #ifdef DS4_NO_GPU return DS4_BACKEND_CPU; #elif defined(__APPLE__) return DS4_BACKEND_METAL; #else return DS4_BACKEND_CUDA; #endif } static server_config parse_options(int argc, char **argv) { server_config c = { .engine = { .model_path = "ds4flash.gguf", .backend = default_server_backend(), .mtp_draft_tokens = 1, .mtp_margin = 3.0f, }, .host = "127.0.0.1", .port = 8000, .ctx_size = 32768, .default_tokens = 393216, .tool_memory_max_ids = DS4_TOOL_MEMORY_DEFAULT_MAX_IDS, .mixed_prefill_quantum = 128, }; c.kv_cache = kv_cache_default_options(); bool directional_steering_scale_set = false; for (int i = 1; i < argc; i++) { const char *arg = argv[i]; if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) { const char *topic = (i + 1 < argc && argv[i + 1][0] != '-') ? argv[i + 1] : NULL; usage(stdout, topic); exit(0); } char dist_parse_err[256] = {0}; ds4_dist_cli_parse_result dist_parse = ds4_dist_parse_cli_arg(arg, &i, argc, argv, &c.engine.distributed, dist_parse_err, sizeof(dist_parse_err)); if (dist_parse == DS4_DIST_CLI_ERROR) { server_log(DS4_LOG_DEFAULT, "ds4-server: %s", dist_parse_err[0] ? dist_parse_err : "invalid distributed option"); exit(2); } if (dist_parse == DS4_DIST_CLI_MATCHED) continue; char tp_parse_err[256] = {0}; ds4_tp_cli_parse_result tp_parse = ds4_tp_parse_cli_arg(arg, &i, argc, argv, &c.engine.tp, tp_parse_err, sizeof(tp_parse_err)); if (tp_parse == DS4_TP_CLI_ERROR) { server_log(DS4_LOG_DEFAULT, "ds4-server: %s", tp_parse_err[0] ? tp_parse_err : "invalid tensor-parallel option"); exit(2); } if (tp_parse == DS4_TP_CLI_MATCHED) continue; if (!strcmp(arg, "-m") || !strcmp(arg, "--model")) { c.engine.model_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--vision")) { c.engine.vision_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--mtp")) { c.engine.glm_mtp = true; } else if (!strcmp(arg, "--mtp-model")) { c.engine.mtp_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--mtp-draft")) { c.engine.mtp_draft_tokens = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--mtp-margin")) { c.engine.mtp_margin = parse_float_arg(need_arg(&i, argc, argv, arg), arg, 0.0f, 1000.0f); } else if (!strcmp(arg, "--mtp-timing")) { c.engine.glm_mtp = true; c.engine.glm_mtp_timing = true; } else if (!strcmp(arg, "--dspark")) { c.engine.dspark = true; } else if (!strcmp(arg, "--dspark-confidence")) { c.engine.dspark = true; c.engine.dspark_confidence_threshold = parse_float_arg(need_arg(&i, argc, argv, arg), arg, 0.0f, 1.0f); c.engine.dspark_confidence_threshold_set = true; } else if (!strcmp(arg, "--dspark-strict")) { c.engine.dspark = true; c.engine.dspark_strict = true; } else if (!strcmp(arg, "--mtp-exact-sampling")) { c.engine.dspark_exact_sampling = true; } else if (!strcmp(arg, "-c") || !strcmp(arg, "--ctx")) { c.ctx_size = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "-n") || !strcmp(arg, "--tokens")) { c.default_tokens = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "-t") || !strcmp(arg, "--threads")) { c.engine.n_threads = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--chdir")) { c.chdir_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--host")) { c.host = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--port")) { c.port = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--cors")) { c.enable_cors = true; } else if (!strcmp(arg, "--trace")) { c.trace_path = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--batched-session")) { c.batched_sessions = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--mixed-prefill-quantum")) { c.mixed_prefill_quantum = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-disk-dir")) { c.kv_disk_dir = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--kv-disk-space-mb")) { c.kv_disk_space_mb = (uint64_t)parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-cache-min-tokens")) { c.kv_cache.min_tokens = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-cache-cold-max-tokens")) { c.kv_cache.cold_max_tokens = parse_nonneg_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-cache-continued-interval-tokens")) { c.kv_cache.continued_interval_tokens = parse_nonneg_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-cache-boundary-trim-tokens")) { c.kv_cache.boundary_trim_tokens = parse_nonneg_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-cache-boundary-align-tokens")) { c.kv_cache.boundary_align_tokens = parse_nonneg_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--kv-cache-reject-different-quant")) { c.kv_cache_reject_different_quant = true; } else if (!strcmp(arg, "--disable-exact-dsml-tool-replay")) { c.disable_exact_dsml_tool_replay = true; } else if (!strcmp(arg, "--tool-memory-max-ids")) { c.tool_memory_max_ids = parse_int_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--quality")) { c.engine.quality = true; } else if (!strcmp(arg, "--ssd-streaming")) { c.engine.ssd_streaming = true; } else if (!strcmp(arg, "--ssd-streaming-cold")) { c.engine.ssd_streaming_cold = true; } else if (!strcmp(arg, "--ssd-streaming-cache-experts")) { uint32_t experts = 0; uint64_t bytes = 0; if (!ds4_parse_streaming_cache_experts_arg( need_arg(&i, argc, argv, arg), &experts, &bytes)) { server_log(DS4_LOG_DEFAULT, "ds4-server: --ssd-streaming-cache-experts must be a positive count or GB"); exit(2); } c.engine.ssd_streaming_cache_experts = experts; c.engine.ssd_streaming_cache_bytes = bytes; } else if (!strcmp(arg, "--ssd-streaming-full-layers")) { int v = parse_nonneg_int_arg(need_arg(&i, argc, argv, arg), arg); c.engine.ssd_streaming_full_layers = (uint32_t)v; c.engine.ssd_streaming_full_layers_set = true; } else if (!strcmp(arg, "--ssd-streaming-preload-experts")) { int v = parse_int_arg(need_arg(&i, argc, argv, arg), arg); if (v <= 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: --ssd-streaming-preload-experts must be positive"); exit(2); } c.engine.ssd_streaming_preload_experts = (uint32_t)v; } else if (!strcmp(arg, "--simulate-used-memory")) { if (!ds4_parse_gib_arg(need_arg(&i, argc, argv, arg), &c.engine.simulate_used_memory_bytes)) { server_log(DS4_LOG_DEFAULT, "ds4-server: --simulate-used-memory must be a positive GiB value, e.g. 64GB"); exit(2); } } else if (!strcmp(arg, "--prefill-chunk")) { int v = parse_int_arg(need_arg(&i, argc, argv, arg), arg); if (v <= 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: --prefill-chunk must be positive"); exit(2); } c.engine.prefill_chunk = (uint32_t)v; } else if (!strcmp(arg, "--power")) { c.engine.power_percent = parse_int_arg(need_arg(&i, argc, argv, arg), arg); if (c.engine.power_percent < 1 || c.engine.power_percent > 100) { server_log(DS4_LOG_DEFAULT, "ds4-server: --power must be between 1 and 100"); exit(2); } } else if (!strcmp(arg, "--dir-steering-file")) { c.engine.directional_steering_file = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--dir-steering-ffn")) { c.engine.directional_steering_ffn = parse_float_arg(need_arg(&i, argc, argv, arg), arg, -100.0f, 100.0f); directional_steering_scale_set = true; } else if (!strcmp(arg, "--dir-steering-attn")) { c.engine.directional_steering_attn = parse_float_arg(need_arg(&i, argc, argv, arg), arg, -100.0f, 100.0f); directional_steering_scale_set = true; } else if (!strcmp(arg, "--warm-weights")) { c.engine.warm_weights = true; } else if (!strcmp(arg, "--metal")) { c.engine.backend = DS4_BACKEND_METAL; #ifdef DS4_ROCM_BUILD } else if (!strcmp(arg, "--rocm")) { c.engine.backend = DS4_BACKEND_CUDA; #else } else if (!strcmp(arg, "--cuda")) { c.engine.backend = DS4_BACKEND_CUDA; #endif } else if (!strcmp(arg, "--gpu-vram")) { c.gpu_vram_arg = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--gpu-devices")) { c.gpu_devices_arg = need_arg(&i, argc, argv, arg); } else if (!strcmp(arg, "--cuda-tensor-parallel")) { c.engine.cuda_tensor_parallel = true; } else if (!strcmp(arg, "--backend")) { c.engine.backend = parse_backend_arg(need_arg(&i, argc, argv, arg), arg); } else if (!strcmp(arg, "--cpu")) { c.engine.backend = DS4_BACKEND_CPU; } else { server_log(DS4_LOG_DEFAULT, "ds4-server: unknown option: %s", arg); usage(stderr, NULL); exit(2); } } if (c.kv_cache.cold_max_tokens > 0 && c.kv_cache.cold_max_tokens < c.kv_cache.min_tokens) { server_log(DS4_LOG_DEFAULT, "ds4-server: --kv-cache-cold-max-tokens must be 0 or >= --kv-cache-min-tokens"); exit(2); } if (c.engine.directional_steering_file && !directional_steering_scale_set) { c.engine.directional_steering_ffn = 1.0f; } char tp_err[256]; if (!ds4_tp_adopt_distributed_options(&c.engine.tp, &c.engine.distributed, tp_err, sizeof(tp_err))) { server_log(DS4_LOG_DEFAULT, "ds4-server: %s", tp_err); exit(2); } char dist_err[256]; if (ds4_dist_prepare_engine_options(&c.engine.distributed, &c.engine, dist_err, sizeof(dist_err)) != 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: %s", dist_err); exit(2); } if (!ds4_tp_validate_engine_options(&c.engine, tp_err, sizeof(tp_err))) { server_log(DS4_LOG_DEFAULT, "ds4-server: %s", tp_err); exit(2); } if (c.engine.tp.role == DS4_TP_WORKER) { server_log(DS4_LOG_DEFAULT, "ds4-server: --role worker is a serving mode; start tensor-parallel workers with ./ds4"); exit(2); } return c; } #ifndef DS4_SERVER_TEST static void server_request_worker_stop(server *s) { pthread_mutex_lock(&s->mu); s->stopping = true; dispatch_jobs_locked(s); pthread_cond_broadcast(&s->cv); pthread_mutex_unlock(&s->mu); } static void server_request_decode_stop(server *s) { if (!s->batched_mode) return; pthread_mutex_lock(&s->model_mu); s->model_stopping = true; pthread_cond_broadcast(&s->model_cv); pthread_mutex_unlock(&s->model_mu); } int main(int argc, char **argv) { signal(SIGPIPE, SIG_IGN); struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = stop_signal_handler; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); server_config cfg = parse_options(argc, argv); if (cfg.chdir_path && chdir(cfg.chdir_path) != 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to chdir to %s: %s", cfg.chdir_path, strerror(errno)); return 1; } cfg.engine.context_size = cfg.ctx_size; cfg.engine.placement_ctx_hint = cfg.ctx_size; cfg.engine.placement_session_count_hint = cfg.batched_sessions > 0 ? cfg.batched_sessions : 1; cfg.engine.share_session_prefill_workspace = cfg.batched_sessions > 0; ds4_engine *engine = NULL; if (cfg.gpu_vram_arg || cfg.gpu_devices_arg) { ds4_gpu_config gpu_cfg = {0}; bool skip_cuda = false; char gpu_err[256]; if (parse_gpu_vram_arg(cfg.gpu_vram_arg, cfg.gpu_devices_arg, &gpu_cfg, &skip_cuda, gpu_err, sizeof(gpu_err)) != 0) { fprintf(stderr, "ds4-server: %s\n", gpu_err); return 2; } cfg.engine.backend = skip_cuda ? DS4_BACKEND_CPU : DS4_BACKEND_CUDA; if (skip_cuda) { if (ds4_engine_open(&engine, &cfg.engine) != 0) return 1; } else { const bool was_auto = (cfg.gpu_vram_arg && !strcmp(cfg.gpu_vram_arg, "auto")) || (!cfg.gpu_vram_arg && cfg.gpu_devices_arg); char layout[256]; if (format_gpu_layout_line(&gpu_cfg, was_auto, layout, sizeof(layout)) > 0) { fprintf(stdout, "%s\n", layout); fflush(stdout); } if (ds4_engine_create_with_gpu_config( &engine, &cfg.engine, &gpu_cfg) != 0) return 1; } } else if (ds4_engine_open(&engine, &cfg.engine) != 0) { return 1; } if (cfg.engine.distributed.role == DS4_DISTRIBUTED_WORKER) { ds4_dist_generation_options gen = { .ctx_size = cfg.ctx_size, }; int rc = ds4_dist_run(engine, &cfg.engine.distributed, &gen); ds4_engine_close(engine); return rc; } ds4_tp *tp_leader = NULL; if (cfg.engine.tp.role == DS4_TP_LEADER) { char tp_err[256] = ""; ds4_tp_identity tp_id = { .gguf_bytes = ds4_engine_model_bytes(engine), .model_id = (uint32_t)ds4_engine_model_id(engine), .n_layer = (uint32_t)ds4_engine_layer_count(engine), .n_embd = (uint32_t)ds4_engine_embd_dim(engine), .n_vocab = (uint32_t)ds4_engine_vocab_size(engine), .quant_bits = (uint32_t)ds4_engine_routed_quant_bits(engine), .ctx_size = (uint32_t)cfg.ctx_size, }; ds4_engine_tp_gate_schedule(engine, &tp_id.gate_slot_start, &tp_id.gate_slot_step, &tp_id.gates_per_token, tp_id.gate_slot_mask); if (!ds4_tp_create(&tp_leader, &cfg.engine.tp, &tp_id, tp_err, sizeof(tp_err)) || !ds4_engine_tp_bind(engine, tp_leader, tp_err, sizeof(tp_err))) { server_log(DS4_LOG_DEFAULT, "ds4-server: %s", tp_err); ds4_tp_free(tp_leader); ds4_engine_close(engine); return 1; } } const int slot_count = cfg.batched_sessions > 0 ? cfg.batched_sessions : 1; log_context_memory(cfg.engine.backend, cfg.ctx_size, ds4_engine_prefill_chunk(engine), cfg.engine.ssd_streaming, slot_count); server s = {0}; s.engine = engine; s.idle_prefill_quantum = (int)ds4_context_memory_estimate_with_prefill_mode( cfg.engine.backend, cfg.ctx_size, ds4_engine_prefill_chunk(engine), cfg.engine.ssd_streaming).prefill_cap; s.tp_leader = tp_leader; s.ctx_size = cfg.ctx_size; s.slot_count = slot_count; s.batched_mode = cfg.batched_sessions > 0; s.qwen4_batch_mtp = cfg.engine.backend == DS4_BACKEND_METAL && ds4_engine_is_qwen4(engine) && ds4_engine_mtp_draft_tokens(engine) > 1; s.mixed_prefill_quantum = cfg.mixed_prefill_quantum; s.last_prefill_slot = slot_count - 1; s.default_tokens = cfg.default_tokens; s.disable_exact_dsml_tool_replay = cfg.disable_exact_dsml_tool_replay; s.tool_mem.max_entries = cfg.tool_memory_max_ids; s.enable_cors = cfg.enable_cors; s.slots = xmalloc((size_t)slot_count * sizeof(*s.slots)); memset(s.slots, 0, (size_t)slot_count * sizeof(*s.slots)); if (s.batched_mode) { s.slot_threads = xmalloc((size_t)slot_count * sizeof(*s.slot_threads)); memset(s.slot_threads, 0, (size_t)slot_count * sizeof(*s.slot_threads)); } pthread_mutex_init(&s.mu, NULL); pthread_cond_init(&s.cv, NULL); pthread_cond_init(&s.clients_cv, NULL); pthread_mutex_init(&s.tool_mu, NULL); pthread_mutex_init(&s.kv_mu, NULL); pthread_mutexattr_t inference_attr; pthread_mutexattr_init(&inference_attr); pthread_mutexattr_settype(&inference_attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&s.inference_mu, &inference_attr); pthread_mutexattr_destroy(&inference_attr); pthread_mutex_init(&s.model_mu, NULL); pthread_cond_init(&s.model_cv, NULL); pthread_mutex_init(&s.trace_mu, NULL); for (int i = 0; i < slot_count; i++) { server_slot *slot = &s.slots[i]; slot->srv = &s; slot->id = i; if (ds4_session_create(&slot->session, engine, cfg.ctx_size) != 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to create %s session %d/%d", ds4_backend_name(cfg.engine.backend), i + 1, slot_count); server_close_resources(&s); return 1; } } if (cfg.kv_disk_dir) { kv_cache_open(&s.kv, cfg.kv_disk_dir, cfg.kv_disk_space_mb, cfg.kv_cache_reject_different_quant, cfg.kv_cache); } if (s.disable_exact_dsml_tool_replay) { server_log(DS4_LOG_DEFAULT, "ds4-server: exact DSML tool replay disabled; tool history uses canonical JSON rendering"); } if (s.batched_mode) { server_log(DS4_LOG_DEFAULT, "ds4-server: batched mode enabled resident_sessions=%d prefill_quantum=%d mixed_prefill_quantum=%d decode_coalesce_us=%ld", s.slot_count, server_prefill_quantum_for(&s, false), server_prefill_quantum_for(&s, true), server_decode_coalesce_us()); if (ds4_engine_mtp_draft_tokens(engine) > 1 && !s.qwen4_batch_mtp) { server_log(DS4_LOG_DEFAULT, "ds4-server: MTP speculative decoding is disabled while native session batching is active"); } } if (cfg.trace_path) { s.trace = fopen(cfg.trace_path, "w"); if (!s.trace) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to open trace file %s: %s", cfg.trace_path, strerror(errno)); server_close_resources(&s); return 1; } setvbuf(s.trace, NULL, _IONBF, 0); server_log(DS4_LOG_DEFAULT, "ds4-server: tracing session to %s", cfg.trace_path); } pthread_t worker = (pthread_t){0}; int slot_threads_started = 0; bool decode_thread_started = false; if (s.batched_mode) { if (pthread_create(&s.decode_thread, NULL, decode_worker_main, &s) != 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to start decode coordinator"); server_close_resources(&s); return 1; } decode_thread_started = true; for (int i = 0; i < s.slot_count; i++) { if (pthread_create(&s.slot_threads[i], NULL, slot_worker_main, &s.slots[i]) != 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to start session worker %d/%d", i + 1, s.slot_count); server_request_worker_stop(&s); for (int j = 0; j < slot_threads_started; j++) { pthread_join(s.slot_threads[j], NULL); } server_request_decode_stop(&s); pthread_join(s.decode_thread, NULL); server_close_resources(&s); return 1; } slot_threads_started++; } } else if (pthread_create(&worker, NULL, worker_main, &s) != 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to start worker"); server_close_resources(&s); return 1; } int lfd = listen_on(cfg.host, cfg.port); if (lfd < 0) { server_log(DS4_LOG_DEFAULT, "ds4-server: failed to listen on %s:%d: %s", cfg.host, cfg.port, strerror(errno)); server_request_worker_stop(&s); if (s.batched_mode) { for (int i = 0; i < slot_threads_started; i++) { pthread_join(s.slot_threads[i], NULL); } server_request_decode_stop(&s); if (decode_thread_started) pthread_join(s.decode_thread, NULL); } else { pthread_join(worker, NULL); } server_close_resources(&s); return 1; } g_listen_fd = lfd; server_log(DS4_LOG_DEFAULT, "ds4-server: listening on http://%s:%d", cfg.host, cfg.port); while (!g_stop_requested) { int fd = accept(lfd, NULL, NULL); if (fd < 0) { if (g_stop_requested) break; if (errno == EINTR) continue; server_log(DS4_LOG_DEFAULT, "ds4-server: accept failed: %s", strerror(errno)); continue; } if (g_stop_requested) { close(fd); break; } configure_client_socket(fd); client_arg *ca = xmalloc(sizeof(*ca)); ca->srv = &s; ca->fd = fd; pthread_mutex_lock(&s.mu); s.clients++; pthread_mutex_unlock(&s.mu); pthread_t th; if (pthread_create(&th, NULL, client_main, ca) != 0) { pthread_mutex_lock(&s.mu); s.clients--; pthread_cond_broadcast(&s.clients_cv); pthread_mutex_unlock(&s.mu); free(ca); close(fd); continue; } pthread_detach(th); } if (g_listen_fd >= 0) { close(lfd); g_listen_fd = -1; } server_log(DS4_LOG_DEFAULT, "ds4-server: shutdown requested, draining requests"); server_request_worker_stop(&s); if (s.batched_mode) { for (int i = 0; i < slot_threads_started; i++) { pthread_join(s.slot_threads[i], NULL); } server_request_decode_stop(&s); if (decode_thread_started) pthread_join(s.decode_thread, NULL); } else { pthread_join(worker, NULL); } pthread_mutex_lock(&s.mu); while (s.clients > 0) pthread_cond_wait(&s.clients_cv, &s.mu); pthread_mutex_unlock(&s.mu); for (int i = 0; s.kv.enabled && i < s.slot_count; i++) { server_slot *slot = &s.slots[i]; const ds4_tokens *tokens = ds4_session_tokens(slot->session); if (!tokens || tokens->len < s.kv.opt.min_tokens) continue; server_log(DS4_LOG_KVCACHE, "ds4-server: persisting resident KV cache before shutdown slot=%d tokens=%d", i, tokens->len); kv_cache_store_current(&s, slot, "shutdown"); } server_close_resources(&s); return 0; } #else static int test_failures = 0; static void test_assert(bool cond, const char *file, int line, const char *expr) { if (cond) return; fprintf(stderr, "%s:%d: assertion failed: %s\n", file, line, expr); test_failures++; } #define TEST_ASSERT(expr) test_assert((expr), __FILE__, __LINE__, #expr) static void test_server_bind_slot(server *s, server_slot *slot) { memset(slot, 0, sizeof(*slot)); slot->srv = s; slot->id = 0; s->slots = slot; s->slot_count = 1; } static void test_batched_prefill_round_robin(void) { server s = {0}; server_slot slots[4] = {0}; s.slots = slots; s.slot_count = 4; s.last_prefill_slot = 0; slots[0].prefill_waiting = true; slots[2].prefill_waiting = true; slots[3].prefill_waiting = true; TEST_ASSERT(server_next_prefill_slot_locked(&s) == 2); s.last_prefill_slot = 2; TEST_ASSERT(server_next_prefill_slot_locked(&s) == 3); slots[3].prefill_waiting = false; TEST_ASSERT(server_next_prefill_slot_locked(&s) == 0); slots[0].prefill_waiting = false; slots[2].prefill_waiting = false; TEST_ASSERT(server_next_prefill_slot_locked(&s) == -1); } static void test_mixed_prefill_quantum_option(void) { char *default_argv[] = {"ds4-server"}; server_config defaults = parse_options(1, default_argv); TEST_ASSERT(defaults.mixed_prefill_quantum == 128); char *custom_argv[] = { "ds4-server", "--mixed-prefill-quantum", "2048" }; server_config custom = parse_options(3, custom_argv); TEST_ASSERT(custom.mixed_prefill_quantum == 2048); server s = {.mixed_prefill_quantum = custom.mixed_prefill_quantum}; TEST_ASSERT(server_prefill_quantum_for(&s, false) == 2048); TEST_ASSERT(server_prefill_quantum_for(&s, true) == 2048); s.mixed_prefill_quantum = defaults.mixed_prefill_quantum; TEST_ASSERT(server_prefill_quantum_for(&s, true) == 128); } /* Reuse-aware slot routing: the probe must recognize every way a request * can reuse a slot's live state, and the router must steer requests that can * reuse nothing away from resident checkpoints. */ static void test_slot_probe_and_routing_scores(void) { server s = {0}; server_slot slots[3] = {0}; s.slots = slots; s.slot_count = 3; /* All checkpoints freshly used: in the protected staleness tier, where * the eviction cost is the checkpoint length (-live semantics). */ const time_t now = 1000000; slots[0].last_used = slots[1].last_used = slots[2].last_used = now; /* slot[0]: resident long checkpoint [sys | main-history] (160 tokens). * slot[1]: resident short checkpoint sharing the sys prefix (101 tokens). * slot[2]: empty. */ int main_tok[160], short_tok[101], sub_tok[120], ext_tok[102]; for (int i = 0; i < 100; i++) main_tok[i] = short_tok[i] = i + 1; for (int i = 100; i < 160; i++) main_tok[i] = 200 + i; short_tok[100] = 300; for (int i = 0; i < 100; i++) sub_tok[i] = i + 1; for (int i = 100; i < 120; i++) sub_tok[i] = 900 + i; for (int i = 0; i < 101; i++) ext_tok[i] = short_tok[i]; ext_tok[101] = 301; slots[0].session = ds4_session_new_test_checkpoint(main_tok, 160); slots[1].session = ds4_session_new_test_checkpoint(short_tok, 101); slots[2].session = ds4_session_new_test_checkpoint(NULL, 0); /* 1. Alien shared-prefix request (a "subagent"): shares 100 tokens with * both resident checkpoints but can reuse neither. It must score * negatively on both (eviction cost, cheapest victim preferred) and * lose to the empty slot. With the old raw common-prefix scoring it * scored +100 on slot[0] - the misroute. */ job sub = {0}; for (int i = 0; i < 120; i++) ds4_tokens_push(&sub.req.prompt, sub_tok[i]); TEST_ASSERT(slot_probe_reuse_locked(&s, &slots[0], &sub.req).kind == REUSE_NONE); TEST_ASSERT(job_slot_score(&s, &slots[0], &sub, -1, now) == SLOT_PROTECTED_SCORE_BASE - 160); TEST_ASSERT(job_slot_score(&s, &slots[1], &sub, -1, now) == SLOT_PROTECTED_SCORE_BASE - 101); TEST_ASSERT(job_slot_score(&s, &slots[2], &sub, -1, now) == 0); /* 2. Extension request: the prompt extends slot[1]'s checkpoint, so it * pins there (+live), and is an eviction elsewhere. */ job ext = {0}; for (int i = 0; i < 102; i++) ds4_tokens_push(&ext.req.prompt, ext_tok[i]); slot_reuse pr = slot_probe_reuse_locked(&s, &slots[1], &ext.req); TEST_ASSERT(pr.kind == REUSE_MEMORY_TOKEN); TEST_ASSERT(pr.reuse_tokens == 101); TEST_ASSERT(job_slot_score(&s, &slots[1], &ext, -1, now) == 101); TEST_ASSERT(job_slot_score(&s, &slots[0], &ext, -1, now) == SLOT_PROTECTED_SCORE_BASE - 160); TEST_ASSERT(job_slot_score(&s, &slots[2], &ext, -1, now) == 0); /* 3. Text-bound request (#62 class): the token prefix diverges, but the * rendered checkpoint text is a byte prefix of the request text. * The probe must still recognize the binding (REUSE_MEMORY_TEXT) and * the slot must stay attractive - otherwise single-session chats with * slots > 1 would ping-pong and re-prefill every turn. */ int stale_tok[3] = {5, 6, 7}; server_slot tslot = {0}; tslot.last_used = now; tslot.session = ds4_session_new_test_checkpoint(stale_tok, 3); tslot.live_text = (char *)"hello world"; tslot.live_text_len = 11; tslot.live_text_pos = 3; job txt = {0}; ds4_tokens_push(&txt.req.prompt, 5); ds4_tokens_push(&txt.req.prompt, 6); ds4_tokens_push(&txt.req.prompt, 999); /* token prefix diverges */ txt.req.prompt_text = (char *)"hello world again"; pr = slot_probe_reuse_locked(&s, &tslot, &txt.req); TEST_ASSERT(pr.kind == REUSE_MEMORY_TEXT); TEST_ASSERT(pr.reuse_tokens == 3); TEST_ASSERT(pr.suffix_off == 11); TEST_ASSERT(job_slot_score(&s, &tslot, &txt, -1, now) == 3); /* 4. Stale rendered-text cache (checkpoint moved past it) must not bind. */ tslot.live_text_pos = 2; TEST_ASSERT(slot_probe_reuse_locked(&s, &tslot, &txt.req).kind == REUSE_NONE); ds4_session_free_test_checkpoint(tslot.session); ds4_session_free_test_checkpoint(slots[0].session); ds4_session_free_test_checkpoint(slots[1].session); ds4_session_free_test_checkpoint(slots[2].session); ds4_tokens_free(&sub.req.prompt); ds4_tokens_free(&ext.req.prompt); ds4_tokens_free(&txt.req.prompt); } /* Staleness tiers: a resident checkpoint idle past SLOT_STALE_AFTER_SEC is * most likely a finished conversation, so it becomes the preferred eviction * victim regardless of its size - while staying fully reusable if its own * conversation continues. This is what keeps long-forgotten sessions from * piling up on a long-running server and pushing active ones out. */ static void test_slot_routing_staleness_tiers(void) { server s = {0}; server_slot slots[4] = {0}; s.slots = slots; s.slot_count = 4; const time_t now = 1000000; /* slot[0]: long checkpoint (160), idle 2h - stale. * slot[1]: short checkpoint (101), idle 1h - stale. * slot[2]: mid checkpoint (120), idle 1min - protected. * slot[3]: empty. */ int long_tok[160], short_tok[101], mid_tok[120]; for (int i = 0; i < 160; i++) long_tok[i] = i + 1; for (int i = 0; i < 101; i++) short_tok[i] = 1000 + i; for (int i = 0; i < 120; i++) mid_tok[i] = 2000 + i; slots[0].session = ds4_session_new_test_checkpoint(long_tok, 160); slots[1].session = ds4_session_new_test_checkpoint(short_tok, 101); slots[2].session = ds4_session_new_test_checkpoint(mid_tok, 120); slots[3].session = ds4_session_new_test_checkpoint(NULL, 0); slots[0].last_used = now - 2 * 3600; slots[1].last_used = now - 1 * 3600; slots[2].last_used = now - 60; /* Alien request: shares no prefix with any checkpoint, reuses nothing. */ job sub = {0}; for (int i = 0; i < 50; i++) ds4_tokens_push(&sub.req.prompt, 9000 + i); /* 1. The empty slot beats every resident checkpoint. */ TEST_ASSERT(job_slot_score(&s, &slots[3], &sub, -1, now) == 0); /* 2. Between stale checkpoints the more idle one is the better victim, * even though it is longer: plain -live scoring would evict slot[1] * and keep slot[0] forever - the finished-session leak. */ TEST_ASSERT(job_slot_score(&s, &slots[0], &sub, -1, now) == -1 - SLOT_IDLE_SCORE_CAP + 2 * 3600); TEST_ASSERT(job_slot_score(&s, &slots[1], &sub, -1, now) == -1 - SLOT_IDLE_SCORE_CAP + 1 * 3600); TEST_ASSERT(job_slot_score(&s, &slots[0], &sub, -1, now) > job_slot_score(&s, &slots[1], &sub, -1, now)); /* 3. Any stale checkpoint is a better victim than any protected one, * even a shorter protected one: an active conversation keeps its * slot while a long-idle finished one is evicted first. */ TEST_ASSERT(job_slot_score(&s, &slots[1], &sub, -1, now) > job_slot_score(&s, &slots[2], &sub, -1, now)); TEST_ASSERT(job_slot_score(&s, &slots[2], &sub, -1, now) == SLOT_PROTECTED_SCORE_BASE - 120); /* 4. Boundary: idle == SLOT_STALE_AFTER_SEC is stale, one second less * is protected. */ slots[2].last_used = now - SLOT_STALE_AFTER_SEC; TEST_ASSERT(job_slot_score(&s, &slots[2], &sub, -1, now) == -1 - SLOT_IDLE_SCORE_CAP + (int)SLOT_STALE_AFTER_SEC); slots[2].last_used = now - SLOT_STALE_AFTER_SEC + 1; TEST_ASSERT(job_slot_score(&s, &slots[2], &sub, -1, now) == SLOT_PROTECTED_SCORE_BASE - 120); slots[2].last_used = now - 60; /* 5. The idle component is capped, so an ancient (or never-stamped, * last_used == 0) checkpoint maxes out at the top of the stale tier * instead of overflowing into another tier's range. */ slots[2].last_used = 0; TEST_ASSERT(job_slot_score(&s, &slots[2], &sub, -1, now + SLOT_IDLE_SCORE_CAP + 5000) == -1); slots[2].last_used = now - 60; /* 6. Staleness never blocks reuse: the owner of a stale checkpoint * continues its conversation and the slot stays the most * attractive - staleness only lowers its priority as a victim. */ job cont = {0}; for (int i = 0; i < 160; i++) ds4_tokens_push(&cont.req.prompt, long_tok[i]); ds4_tokens_push(&cont.req.prompt, 500); TEST_ASSERT(slot_probe_reuse_locked(&s, &slots[0], &cont.req).kind == REUSE_MEMORY_TOKEN); TEST_ASSERT(job_slot_score(&s, &slots[0], &cont, -1, now) == 160); TEST_ASSERT(job_slot_score(&s, &slots[0], &cont, -1, now) > job_slot_score(&s, &slots[3], &cont, -1, now)); ds4_session_free_test_checkpoint(slots[0].session); ds4_session_free_test_checkpoint(slots[1].session); ds4_session_free_test_checkpoint(slots[2].session); ds4_session_free_test_checkpoint(slots[3].session); ds4_tokens_free(&sub.req.prompt); ds4_tokens_free(&cont.req.prompt); } static void test_slot_probe_live_state_tiers(void) { server s = {0}; int ckpt_tok[10]; for (int i = 0; i < 10; i++) ckpt_tok[i] = i + 1; /* 1. responses-visible: the visible transcript remembered at the live * frontier is a byte prefix of the replayed request text. */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.responses_live.valid = true; slot.responses_live.live_tokens = 10; slot.responses_live.visible_text = xstrdup("turn1 "); slot.responses_live.visible_len = strlen(slot.responses_live.visible_text); job j = {0}; j.req.api = API_RESPONSES; j.req.prompt_text = xstrdup("turn1 turn2"); ds4_tokens_push(&j.req.prompt, 999); /* token prefix diverges */ slot_reuse pr = slot_probe_reuse_locked(&s, &slot, &j.req); TEST_ASSERT(pr.kind == REUSE_RESPONSES_VISIBLE); TEST_ASSERT(pr.reuse_tokens == 10); TEST_ASSERT(pr.suffix_off == slot.responses_live.visible_len); /* Wrong frontier: the remembered state must name the live position. */ slot.responses_live.live_tokens = 9; TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &j.req).kind == REUSE_NONE); live_tool_state_free(&slot.responses_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } /* 2. responses-tool-output: the call-id frontier binds the request even * with no visible prefix at all. */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.responses_live.valid = true; slot.responses_live.live_tokens = 10; id_list_push_unique(&slot.responses_live.call_ids, "call-abc"); job j = {0}; j.req.api = API_RESPONSES; j.req.responses_live_suffix_text = xstrdup(" tool-out"); id_list_push_unique(&j.req.responses_live_call_ids, "call-abc"); ds4_tokens_push(&j.req.prompt, 999); slot_reuse pr = slot_probe_reuse_locked(&s, &slot, &j.req); TEST_ASSERT(pr.kind == REUSE_RESPONSES_TOOL_OUTPUT); TEST_ASSERT(pr.reuse_tokens == 10); TEST_ASSERT(pr.matched_ids == 1); /* A different id set must not bind. */ stop_list_clear(&j.req.responses_live_call_ids); free(j.req.responses_live_call_ids.v); memset(&j.req.responses_live_call_ids, 0, sizeof(j.req.responses_live_call_ids)); id_list_push_unique(&j.req.responses_live_call_ids, "call-other"); TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &j.req).kind == REUSE_NONE); live_tool_state_free(&slot.responses_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } /* 3. anthropic-tool-output: same binding via tool_use_id. */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.anthropic_live.valid = true; slot.anthropic_live.live_tokens = 10; id_list_push_unique(&slot.anthropic_live.call_ids, "toolu-1"); job j = {0}; j.req.api = API_ANTHROPIC; j.req.anthropic_live_suffix_text = xstrdup(" result"); id_list_push_unique(&j.req.anthropic_live_call_ids, "toolu-1"); ds4_tokens_push(&j.req.prompt, 999); slot_reuse pr = slot_probe_reuse_locked(&s, &slot, &j.req); TEST_ASSERT(pr.kind == REUSE_ANTHROPIC_TOOL_OUTPUT); TEST_ASSERT(pr.reuse_tokens == 10); TEST_ASSERT(pr.matched_ids == 1); live_tool_state_free(&slot.anthropic_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } /* 4. thinking-visible: a chat request whose replay omits hidden thinking * binds via the remembered visible transcript; skipped for Responses * (which has its own visible tier). */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.thinking_live.valid = true; slot.thinking_live.live_tokens = 10; slot.thinking_live.visible_text = xstrdup("conv so far"); slot.thinking_live.visible_len = strlen(slot.thinking_live.visible_text); job j = {0}; j.req.kind = REQ_CHAT; j.req.api = API_OPENAI; j.req.prompt_text = xstrdup("conv so far plus new question"); ds4_tokens_push(&j.req.prompt, 999); slot_reuse pr = slot_probe_reuse_locked(&s, &slot, &j.req); TEST_ASSERT(pr.kind == REUSE_THINKING_VISIBLE); TEST_ASSERT(pr.reuse_tokens == 10); TEST_ASSERT(pr.suffix_off == slot.thinking_live.visible_len); j.req.api = API_RESPONSES; /* thinking tier must skip Responses */ TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &j.req).kind == REUSE_NONE); visible_live_free(&slot.thinking_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } /* 5. Tier precedence: a request matching both responses-visible and * memory-token is resolved as responses-visible (ladder order). */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.responses_live.valid = true; slot.responses_live.live_tokens = 10; slot.responses_live.visible_text = xstrdup("v "); slot.responses_live.visible_len = 2; job j = {0}; j.req.api = API_RESPONSES; j.req.prompt_text = xstrdup("v more"); for (int i = 0; i < 10; i++) ds4_tokens_push(&j.req.prompt, i + 1); ds4_tokens_push(&j.req.prompt, 11); /* exact extension: memory-token */ TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &j.req).kind == REUSE_RESPONSES_VISIBLE); live_tool_state_free(&slot.responses_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } /* 6. An alien request binds nowhere even when every live-state tier is * populated: discrimination for shared-prefix subagents. */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.thinking_live.valid = true; slot.thinking_live.live_tokens = 10; slot.thinking_live.visible_text = xstrdup("main conv"); slot.thinking_live.visible_len = 9; slot.responses_live.valid = true; slot.responses_live.live_tokens = 10; slot.responses_live.visible_text = xstrdup("main conv"); slot.responses_live.visible_len = 9; job j = {0}; j.req.kind = REQ_CHAT; j.req.api = API_OPENAI; j.req.prompt_text = xstrdup("main different task"); for (int i = 0; i < 5; i++) ds4_tokens_push(&j.req.prompt, i + 1); TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &j.req).kind == REUSE_NONE); slot.last_used = time(NULL); TEST_ASSERT(job_slot_score(&s, &slot, &j, -1, time(NULL)) == SLOT_PROTECTED_SCORE_BASE - 10); live_tool_state_free(&slot.responses_live); visible_live_free(&slot.thinking_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } } /* Vision tiers: image appends must reuse, pixel mismatches must not, and * visible replay must survive request-local image-marker nonces. Regression * coverage for the 81a7658 merge (live first-image / thinking-new-image * misses): the probe gate is fingerprint-prefix, and visible tiers compare * in visible_prompt_key space. */ static void test_slot_probe_vision_tiers(void) { server s = {0}; const time_t now = 1000000; int ckpt_tok[10]; for (int i = 0; i < 10; i++) ckpt_tok[i] = i + 1; /* Token equality cannot authenticate a moved image. Unlike a visible * continuation, this tier does not rebuild or rebase the image spans. */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); ds4_vision_span image = {.token_start = 2, .embedding = {.token_count = 2, .fingerprint = {7}}}; ds4_session_set_test_images(slot.session, &image, 1); request req = {0}; for (int i = 0; i < 10; i++) ds4_tokens_push(&req.prompt, ckpt_tok[i]); ds4_tokens_push(&req.prompt, 11); req.images = ℑ req.image_count = 1; TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &req).kind == REUSE_MEMORY_TOKEN); image.token_start++; TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &req).kind == REUSE_NONE); ds4_tokens_free(&req.prompt); ds4_session_free_test_checkpoint(slot.session); } /* 1. Text checkpoint + appended image reuses via memory-text. The old * exact-state gate returned REUSE_NONE here (first-image live miss: * full re-prefill instead of prefix reuse). */ { server_slot slots[2] = {0}; slots[0].last_used = slots[1].last_used = now; slots[0].session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slots[0].live_text = xstrdup("hello world"); slots[0].live_text_len = 11; slots[0].live_text_pos = 10; slots[1].session = ds4_session_new_test_checkpoint(NULL, 0); job j = {0}; ds4_tokens_push(&j.req.prompt, 999); /* force text tier */ j.req.prompt_text = xstrdup("hello world "); j.req.images = calloc(1, sizeof(*j.req.images)); j.req.images[0].token_start = 10; j.req.images[0].embedding.token_count = 16; j.req.images[0].embedding.fingerprint[0] = 7; j.req.image_count = 1; slot_reuse pr = slot_probe_reuse_locked(&s, &slots[0], &j.req); TEST_ASSERT(pr.kind == REUSE_MEMORY_TEXT); TEST_ASSERT(pr.reuse_tokens == 10); TEST_ASSERT(pr.suffix_off == 11); TEST_ASSERT(job_slot_score(&s, &slots[0], &j, -1, now) == 10); TEST_ASSERT(job_slot_score(&s, &slots[0], &j, -1, now) > job_slot_score(&s, &slots[1], &j, -1, now)); ds4_session_free_test_checkpoint(slots[0].session); ds4_session_free_test_checkpoint(slots[1].session); free(slots[0].live_text); request_free(&j.req); } /* 2. Pixel mismatch binds nowhere: same text, different fingerprint. * Materialization would fail rebase, so the probe must already say * NONE -- otherwise routing evicts a good checkpoint for a cold * prefill (changed-image class). */ { server_slot slots[2] = {0}; slots[0].last_used = slots[1].last_used = now; slots[0].session = ds4_session_new_test_checkpoint(ckpt_tok, 10); ds4_vision_span live_img = {0}; live_img.token_start = 10; live_img.embedding.token_count = 16; live_img.embedding.fingerprint[0] = 7; ds4_session_set_test_images(slots[0].session, &live_img, 1); slots[0].live_text = xstrdup("hello world"); slots[0].live_text_len = 11; slots[0].live_text_pos = 10; slots[1].session = ds4_session_new_test_checkpoint(NULL, 0); job j = {0}; ds4_tokens_push(&j.req.prompt, 999); j.req.prompt_text = xstrdup("hello world "); j.req.images = calloc(1, sizeof(*j.req.images)); j.req.images[0].token_start = 10; j.req.images[0].embedding.token_count = 16; j.req.images[0].embedding.fingerprint[0] = 8; /* changed pixels */ j.req.image_count = 1; TEST_ASSERT(slot_probe_reuse_locked(&s, &slots[0], &j.req).kind == REUSE_NONE); TEST_ASSERT(job_slot_score(&s, &slots[1], &j, -1, now) == 0); TEST_ASSERT(job_slot_score(&s, &slots[1], &j, -1, now) > job_slot_score(&s, &slots[0], &j, -1, now)); /* Positive control: matching fingerprint reuses. */ j.req.images[0].embedding.fingerprint[0] = 7; slot_reuse pr = slot_probe_reuse_locked(&s, &slots[0], &j.req); TEST_ASSERT(pr.kind == REUSE_MEMORY_TEXT); TEST_ASSERT(pr.reuse_tokens == 10); ds4_session_free_test_checkpoint(slots[0].session); ds4_session_free_test_checkpoint(slots[1].session); free(slots[0].live_text); request_free(&j.req); } /* 3. thinking-visible survives marker nonces: the stored key masks * markers to \036, so a replay with different nonces at the same * offsets still binds; a shifted image does not. */ { server_slot slot = {0}; slot.session = ds4_session_new_test_checkpoint(ckpt_tok, 10); slot.thinking_live.valid = true; slot.thinking_live.live_tokens = 10; slot.thinking_live.visible_text = xstrdup("see \036\036\036\036\036\036\036 now?"); slot.thinking_live.visible_len = strlen(slot.thinking_live.visible_text); slot.thinking_live.images.count = 1; slot.thinking_live.images.offsets[0] = 4; job j = {0}; j.req.kind = REQ_CHAT; j.req.api = API_OPENAI; j.req.prompt_text = xstrdup("see now? and more"); j.req.image_markers = xmalloc(sizeof(*j.req.image_markers)); snprintf(j.req.image_markers[0], sizeof(j.req.image_markers[0]), ""); j.req.images = calloc(1, sizeof(*j.req.images)); j.req.images[0].token_start = 16; j.req.images[0].embedding.token_count = 16; j.req.images[0].embedding.fingerprint[0] = 9; j.req.image_count = 1; ds4_tokens_push(&j.req.prompt, 999); slot_reuse pr = slot_probe_reuse_locked(&s, &slot, &j.req); TEST_ASSERT(pr.kind == REUSE_THINKING_VISIBLE); TEST_ASSERT(pr.reuse_tokens == 10); /* Same text, image shifted: offset mismatch must not bind. */ free(j.req.prompt_text); j.req.prompt_text = xstrdup("see now? later and more"); TEST_ASSERT(slot_probe_reuse_locked(&s, &slot, &j.req).kind == REUSE_NONE); visible_live_free(&slot.thinking_live); ds4_session_free_test_checkpoint(slot.session); request_free(&j.req); } } static void test_dispatch_routes_alien_request_to_empty_slot(void) { server s = {0}; pthread_mutex_init(&s.tool_mu, NULL); pthread_cond_init(&s.cv, NULL); s.batched_mode = true; server_slot slots[2] = {0}; s.slots = slots; s.slot_count = 2; int main_tok[50]; for (int i = 0; i < 50; i++) main_tok[i] = i + 1; slots[0].session = ds4_session_new_test_checkpoint(main_tok, 50); slots[1].session = ds4_session_new_test_checkpoint(NULL, 0); /* Alien request sharing no prefix at all: the old router tied at 0 and * picked slot[0] (lowest index), evicting the resident checkpoint. */ job j = {0}; ds4_tokens_push(&j.req.prompt, 777); ds4_tokens_push(&j.req.prompt, 888); s.head = &j; s.tail = &j; dispatch_jobs_locked(&s); TEST_ASSERT(slots[1].assigned == &j); TEST_ASSERT(slots[1].busy); TEST_ASSERT(slots[0].assigned == NULL); TEST_ASSERT(s.head == NULL); ds4_tokens_free(&j.req.prompt); ds4_session_free_test_checkpoint(slots[0].session); ds4_session_free_test_checkpoint(slots[1].session); pthread_mutex_destroy(&s.tool_mu); pthread_cond_destroy(&s.cv); } static void test_multimodal_prefill_resume_frontier(void) { TEST_ASSERT(server_multimodal_resume_frontier(160, 160, 170, true) == 160); TEST_ASSERT(server_multimodal_resume_frontier(160, 159, 170, true) == 0); TEST_ASSERT(server_multimodal_resume_frontier(160, 160, 159, true) == 0); TEST_ASSERT(server_multimodal_resume_frontier(160, 160, 170, false) == 0); } static void test_batched_live_continuation_slot_binding(void) { server s = {0}; server_slot slots[3] = {0}; s.slots = slots; s.slot_count = 3; job j = {0}; j.req.responses_requires_live_tool_state = true; id_list_push_unique(&j.req.responses_live_call_ids, "call-slot-1"); slots[1].responses_live.valid = true; id_list_push_unique(&slots[1].responses_live.call_ids, "call-slot-1"); TEST_ASSERT(job_required_slot_locked(&s, &j) == 1); j.req.responses_requires_live_tool_state = false; j.req.anthropic_requires_live_tool_state = true; id_list_push_unique(&j.req.anthropic_live_call_ids, "toolu-slot-2"); slots[2].anthropic_live.valid = true; id_list_push_unique(&slots[2].anthropic_live.call_ids, "toolu-slot-2"); TEST_ASSERT(job_required_slot_locked(&s, &j) == 2); slots[2].anthropic_live.valid = false; TEST_ASSERT(job_required_slot_locked(&s, &j) == -1); request_free(&j.req); live_tool_state_free(&slots[1].responses_live); live_tool_state_free(&slots[2].anthropic_live); } /* A tool-output-only continuation has no replayable history and no disk key, so * it must be rejected whenever the live frontier could not be materialized -- * including when the probe selected a tier but building the prompt failed * (unrepresentable suffix, mismatched image identity accounting). Serving it * anyway would generate from a context-free prompt built from tool outputs * alone. Requests that replay their history keep the disk/cold fallback. */ static void test_live_continuation_contract(void) { request r = {0}; /* Responses tool-output-only: rejected until a live tier materializes. */ r.api = API_RESPONSES; r.responses_requires_live_tool_state = true; TEST_ASSERT(live_continuation_unavailable(&r, false)); TEST_ASSERT(!live_continuation_unavailable(&r, true)); r.responses_requires_live_tool_state = false; TEST_ASSERT(!live_continuation_unavailable(&r, false)); /* Anthropic tool-result-only behaves the same way. */ r.api = API_ANTHROPIC; r.anthropic_requires_live_tool_state = true; TEST_ASSERT(live_continuation_unavailable(&r, false)); TEST_ASSERT(!live_continuation_unavailable(&r, true)); r.anthropic_requires_live_tool_state = false; TEST_ASSERT(!live_continuation_unavailable(&r, false)); /* Wrong protocol flag on a given API must not trigger the 409: the parser * only sets the flag matching the request's API. */ r.api = API_RESPONSES; r.anthropic_requires_live_tool_state = true; TEST_ASSERT(!live_continuation_unavailable(&r, false)); /* Chat/completions has no live-only continuation: it always keeps the * prefix/disk fallback. */ r.api = API_OPENAI; r.responses_requires_live_tool_state = true; TEST_ASSERT(!live_continuation_unavailable(&r, false)); TEST_ASSERT(!live_continuation_unavailable(NULL, false)); } static void test_tool_schema_order_from_anthropic_schema(void) { tool_schema_orders orders = {0}; tool_schema_orders_add_json(&orders, "{\"name\":\"bash\",\"input_schema\":{\"type\":\"object\",\"properties\":{" "\"command\":{\"type\":\"string\"}," "\"description\":{\"type\":\"string\"}}}}"); const tool_schema_order *order = tool_schema_orders_find(&orders, "bash"); TEST_ASSERT(order != NULL); TEST_ASSERT(order && order->len == 2); TEST_ASSERT(order && !strcmp(order->prop[0], "command")); TEST_ASSERT(order && !strcmp(order->prop[1], "description")); tool_schema_orders_free(&orders); } static void test_tool_schema_order_from_openai_tools(void) { const char *json = "[{\"type\":\"function\",\"function\":{\"name\":\"edit\",\"parameters\":{" "\"type\":\"object\",\"properties\":{" "\"filePath\":{\"type\":\"string\"}," "\"oldString\":{\"type\":\"string\"}," "\"newString\":{\"type\":\"string\"}}}}}]"; const char *p = json; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&p, &schemas, &orders)); TEST_ASSERT(schemas && strstr(schemas, "\"name\":\"edit\"")); const tool_schema_order *order = tool_schema_orders_find(&orders, "edit"); TEST_ASSERT(order != NULL); TEST_ASSERT(order && order->len == 3); TEST_ASSERT(order && !strcmp(order->prop[0], "filePath")); TEST_ASSERT(order && !strcmp(order->prop[1], "oldString")); TEST_ASSERT(order && !strcmp(order->prop[2], "newString")); free(schemas); tool_schema_orders_free(&orders); } static void test_tool_schema_order_from_responses_tool_search(void) { const char *json = "[{\"type\":\"tool_search\",\"execution\":\"client\"," "\"description\":\"Search deferred tools\"," "\"parameters\":{\"type\":\"object\",\"properties\":{" "\"query\":{\"type\":\"string\"}," "\"limit\":{\"type\":\"number\"}},\"required\":[\"query\"]}}]"; const char *p = json; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&p, &schemas, &orders)); TEST_ASSERT(schemas && strstr(schemas, "\"name\":\"tool_search\"")); TEST_ASSERT(schemas && strstr(schemas, "\"description\":\"Search deferred tools\"")); const tool_schema_order *order = tool_schema_orders_find(&orders, "tool_search"); TEST_ASSERT(order != NULL); TEST_ASSERT(order && order->responses_tool_search); TEST_ASSERT(order && order->len == 2); TEST_ASSERT(order && !strcmp(order->prop[0], "query")); TEST_ASSERT(order && !strcmp(order->prop[1], "limit")); free(schemas); tool_schema_orders_free(&orders); } static void test_responses_function_named_tool_search_stays_function_call(void) { const char *json = "[{\"type\":\"function\",\"function\":{\"name\":\"tool_search\"," "\"description\":\"A normal user function that happens to use a reserved name\"," "\"parameters\":{\"type\":\"object\",\"properties\":{" "\"query\":{\"type\":\"string\"}}}}}]"; const char *p = json; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&p, &schemas, &orders)); const tool_schema_order *order = tool_schema_orders_find(&orders, "tool_search"); TEST_ASSERT(order != NULL); TEST_ASSERT(order && !order->responses_tool_search); tool_calls calls = {0}; tool_call tc = {0}; tc.id = xstrdup("call_user_tool_search"); tc.name = xstrdup("tool_search"); tc.arguments = xstrdup("{\"query\":\"plain function\"}"); tool_calls_push(&calls, tc); responses_tool_item item = { .fc_id = "fc_user_tool_search", .call_id = "call_user_tool_search", .is_custom = false, .output_index = 0, }; buf out = {0}; responses_append_function_call_item(&out, &calls.v[0], &item, "completed", true, &orders); TEST_ASSERT(strstr(out.ptr, "\"type\":\"function_call\"") != NULL); TEST_ASSERT(strstr(out.ptr, "\"type\":\"tool_search_call\"") == NULL); buf_free(&out); tool_calls_free(&calls); free(schemas); tool_schema_orders_free(&orders); } static void test_responses_namespace_tool_schemas_restore_wire_namespace(void) { const char *json = "[{\"type\":\"namespace\",\"name\":\"mcp__perplexity__\"," "\"description\":\"Perplexity tools\"," "\"tools\":[{\"type\":\"function\",\"name\":\"perplexity_search\"," "\"description\":\"Search the web\"," "\"parameters\":{\"type\":\"object\",\"properties\":{" "\"query\":{\"type\":\"string\"}," "\"recency\":{\"type\":\"number\"}}}}]}]"; const char *p = json; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&p, &schemas, &orders)); TEST_ASSERT(schemas && strstr(schemas, "\"name\":\"mcp__perplexity__perplexity_search\"")); TEST_ASSERT(schemas && strstr(schemas, "\"name\":\"perplexity_search\"") == NULL); const tool_schema_order *order = tool_schema_orders_find(&orders, "mcp__perplexity__perplexity_search"); TEST_ASSERT(order != NULL); TEST_ASSERT(order && order->namespace && !strcmp(order->namespace, "mcp__perplexity__")); TEST_ASSERT(order && order->wire_name && !strcmp(order->wire_name, "perplexity_search")); TEST_ASSERT(order && order->len == 2); tool_calls calls = {0}; tool_call tc = {0}; tc.id = xstrdup("call_ns"); tc.name = xstrdup("mcp__perplexity__perplexity_search"); tc.arguments = xstrdup("{\"query\":\"deepseek\",\"recency\":7}"); tool_calls_push(&calls, tc); responses_tool_item item = { .fc_id = "fc_ns", .call_id = "call_ns", .is_custom = false, .output_index = 0, }; buf out = {0}; responses_append_function_call_item(&out, &calls.v[0], &item, "completed", true, &orders); TEST_ASSERT(strstr(out.ptr, "\"name\":\"perplexity_search\"") != NULL); TEST_ASSERT(strstr(out.ptr, "\"namespace\":\"mcp__perplexity__\"") != NULL); TEST_ASSERT(strstr(out.ptr, "mcp__perplexity__perplexity_search") == NULL); buf_free(&out); tool_calls_free(&calls); free(schemas); tool_schema_orders_free(&orders); } static void test_responses_input_tool_search_output_loads_tools(void) { const char *json = "[" "{\"type\":\"tool_search_call\",\"call_id\":\"call_search\"," "\"execution\":\"client\",\"arguments\":{\"query\":\"perplexity\"}}," "{\"type\":\"tool_search_output\",\"call_id\":\"call_search\"," "\"status\":\"completed\",\"execution\":\"client\",\"tools\":[" "{\"type\":\"namespace\",\"name\":\"mcp__perplexity__\"," "\"description\":\"Perplexity tools\"," "\"tools\":[{\"type\":\"function\",\"name\":\"perplexity_search\"," "\"description\":\"Search with Perplexity\"," "\"parameters\":{\"type\":\"object\",\"properties\":{" "\"query\":{\"type\":\"string\"}}}}]}]}" "]"; const char *p = json; chat_msgs msgs = {0}; buf loaded = {0}; tool_schema_orders orders = {0}; TEST_ASSERT(parse_responses_input(&p, &msgs, &loaded, &orders)); TEST_ASSERT(loaded.ptr && strstr(loaded.ptr, "\"name\":\"mcp__perplexity__perplexity_search\"")); const tool_schema_order *order = tool_schema_orders_find(&orders, "mcp__perplexity__perplexity_search"); TEST_ASSERT(order != NULL); TEST_ASSERT(order && order->namespace && !strcmp(order->namespace, "mcp__perplexity__")); TEST_ASSERT(order && order->wire_name && !strcmp(order->wire_name, "perplexity_search")); TEST_ASSERT(msgs.len == 2); TEST_ASSERT(msgs.v[0].calls.len == 1); TEST_ASSERT(!strcmp(msgs.v[0].calls.v[0].name, "tool_search")); TEST_ASSERT(strstr(msgs.v[1].content, "mcp__perplexity__") != NULL); buf_free(&loaded); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); } static void test_responses_input_tool_search_output_rejects_bad_tools(void) { const char *json = "[{\"type\":\"tool_search_output\",\"call_id\":\"call_search\"," "\"status\":\"completed\",\"tools\":{\"not\":\"a tool array\"}}]"; const char *p = json; chat_msgs msgs = {0}; buf loaded = {0}; tool_schema_orders orders = {0}; TEST_ASSERT(!parse_responses_input(&p, &msgs, &loaded, &orders)); buf_free(&loaded); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); } static void test_responses_input_function_call_namespace_round_trips_to_dsml(void) { const char *tools_json = "[{\"type\":\"namespace\",\"name\":\"mcp__perplexity__\"," "\"tools\":[{\"type\":\"function\",\"name\":\"perplexity_search\"," "\"parameters\":{\"type\":\"object\",\"properties\":{" "\"query\":{\"type\":\"string\"}}}}]}]"; const char *tools_p = tools_json; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&tools_p, &schemas, &orders)); const char *input_json = "[{\"type\":\"function_call\",\"call_id\":\"call_ns\"," "\"name\":\"perplexity_search\",\"namespace\":\"mcp__perplexity__\"," "\"arguments\":{\"query\":\"deepseek\"}}]"; const char *input_p = input_json; chat_msgs msgs = {0}; TEST_ASSERT(parse_responses_input(&input_p, &msgs, NULL, NULL)); TEST_ASSERT(msgs.len == 1); TEST_ASSERT(msgs.v[0].calls.len == 1); TEST_ASSERT(!strcmp(msgs.v[0].calls.v[0].name, "mcp__perplexity__perplexity_search")); char *prompt = render_chat_prompt_text(&msgs, schemas, &orders, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "<|DSML|invoke name=\"mcp__perplexity__perplexity_search\">") != NULL); TEST_ASSERT(strstr(prompt, "<|DSML|invoke name=\"perplexity_search\">") == NULL); free(prompt); chat_msgs_free(&msgs); free(schemas); tool_schema_orders_free(&orders); } static void test_responses_output_sends_tool_search_call_item(void) { tool_calls calls = {0}; tool_call tc = {0}; tc.id = xstrdup("call_search"); tc.name = xstrdup("tool_search"); tc.arguments = xstrdup("{\"limit\":3,\"query\":\"perplexity\"}"); tool_calls_push(&calls, tc); const char *tools_json = "[{\"type\":\"tool_search\",\"execution\":\"client\"," "\"parameters\":{\"type\":\"object\",\"properties\":{" "\"query\":{\"type\":\"string\"},\"limit\":{\"type\":\"number\"}}}}]"; const char *tools_p = tools_json; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&tools_p, &schemas, &orders)); responses_tool_item item = { .fc_id = "fc_search", .call_id = "call_search", .is_custom = false, .output_index = 0, }; buf out = {0}; responses_append_function_call_item(&out, &calls.v[0], &item, "completed", true, &orders); TEST_ASSERT(strstr(out.ptr, "\"type\":\"tool_search_call\"") != NULL); TEST_ASSERT(strstr(out.ptr, "\"execution\":\"client\"") != NULL); TEST_ASSERT(strstr(out.ptr, "\"status\":\"completed\"") != NULL); TEST_ASSERT(strstr(out.ptr, "\"arguments\":{\"limit\":3,\"query\":\"perplexity\"}") != NULL); TEST_ASSERT(strstr(out.ptr, "\"type\":\"function_call\"") == NULL); buf_free(&out); free(schemas); tool_schema_orders_free(&orders); tool_calls_free(&calls); } static tool_calls make_swapped_bash_call(void) { tool_calls calls = {0}; tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"description\":\"list files\",\"command\":\"ls -la\",\"timeout\":10}"); tool_calls_push(&calls, tc); return calls; } static tool_schema_orders make_bash_order(void) { tool_schema_orders orders = {0}; tool_schema_orders_add_json(&orders, "{\"name\":\"bash\",\"input_schema\":{\"type\":\"object\",\"properties\":{" "\"command\":{\"type\":\"string\"}," "\"description\":{\"type\":\"string\"}}}}"); return orders; } static char *read_socket_text(int fd) { buf b = {0}; char tmp[1024]; ssize_t n; while ((n = read(fd, tmp, sizeof(tmp))) > 0) { buf_append(&b, tmp, (size_t)n); } return buf_take(&b); } static void test_context_length_error_uses_protocol_standard_shape(void) { request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.prompt.len = 16; TEST_ASSERT(request_exceeds_context(&r, 16)); TEST_ASSERT(!request_exceeds_context(&r, 17)); int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] >= 0 && sv[1] >= 0) { TEST_ASSERT(http_error_context_length_exceeded(sv[0], false, &r, 16, 16)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "HTTP/1.1 400") != NULL); TEST_ASSERT(strstr(out, "\"type\":\"invalid_request_error\"") != NULL); TEST_ASSERT(strstr(out, "\"code\":\"context_length_exceeded\"") != NULL); TEST_ASSERT(strstr(out, "\"param\":\"messages\"") != NULL); TEST_ASSERT(strstr(out, "\"n_prompt_tokens\":16") != NULL); TEST_ASSERT(strstr(out, "\"n_ctx\":16") != NULL); free(out); close(sv[0]); close(sv[1]); } request_free(&r); request a; request_init(&a, REQ_CHAT, 128); a.api = API_ANTHROPIC; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] >= 0 && sv[1] >= 0) { TEST_ASSERT(http_error_context_length_exceeded(sv[0], false, &a, 20, 20)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "{\"type\":\"error\",\"error\"") != NULL); TEST_ASSERT(strstr(out, "\"type\":\"invalid_request_error\"") != NULL); TEST_ASSERT(strstr(out, "\"n_prompt_tokens\":20") != NULL); free(out); close(sv[0]); close(sv[1]); } request_free(&a); } static void test_cors_headers_are_opt_in(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] >= 0 && sv[1] >= 0) { TEST_ASSERT(http_response(sv[0], false, 200, "application/json", "{}")); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "HTTP/1.1 200 OK") != NULL); TEST_ASSERT(strstr(out, "Access-Control-Allow-Origin") == NULL); free(out); close(sv[0]); close(sv[1]); } TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] >= 0 && sv[1] >= 0) { TEST_ASSERT(http_response(sv[0], true, 200, "application/json", "{}")); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "HTTP/1.1 200 OK") != NULL); TEST_ASSERT(strstr(out, "Access-Control-Allow-Origin: *") != NULL); TEST_ASSERT(strstr(out, "Access-Control-Allow-Methods: GET, POST, OPTIONS") != NULL); TEST_ASSERT(strstr(out, "Access-Control-Allow-Headers: *") != NULL); free(out); close(sv[0]); close(sv[1]); } } static void test_cors_preflight_response_is_no_content(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; TEST_ASSERT(http_response(sv[0], true, 204, NULL, "")); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "HTTP/1.1 204 No Content") != NULL); TEST_ASSERT(strstr(out, "Content-Length: 0") != NULL); TEST_ASSERT(strstr(out, "Content-Type:") == NULL); TEST_ASSERT(strstr(out, "Access-Control-Allow-Origin: *") != NULL); free(out); close(sv[0]); close(sv[1]); } static void test_cors_sse_headers(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; TEST_ASSERT(sse_headers(sv[0], true)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "HTTP/1.1 200 OK") != NULL); TEST_ASSERT(strstr(out, "Content-Type: text/event-stream") != NULL); TEST_ASSERT(strstr(out, "Access-Control-Allow-Origin: *") != NULL); free(out); close(sv[0]); close(sv[1]); } static void test_anthropic_live_stream_sends_incremental_blocks(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_ANTHROPIC; r.stream = true; r.think_mode = DS4_THINK_HIGH; r.has_tools = true; r.tool_orders = make_bash_order(); anthropic_stream st; TEST_ASSERT(anthropic_sse_start_live(sv[0], &r, "msg_test", 10, &st)); const char *raw1 = "need a toolHello.\n\n"; TEST_ASSERT(anthropic_sse_stream_update(sv[0], NULL, &r, "msg_test", &st, raw1, strlen(raw1), false)); const char *raw = "need a toolHello.\n\n" DS4_TOOL_CALLS_START "\n"; TEST_ASSERT(anthropic_sse_stream_update(sv[0], NULL, &r, "msg_test", &st, raw, strlen(raw), false)); tool_calls calls = make_swapped_bash_call(); TEST_ASSERT(anthropic_sse_finish_live(sv[0], NULL, &r, "msg_test", &st, raw, strlen(raw), &calls, "tool_calls", 8)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); const char *msg_start = strstr(out, "event: message_start"); const char *thinking = strstr(out, "\"thinking\":\"need a tool\""); const char *signature = strstr(out, "\"type\":\"signature_delta\""); const char *text = strstr(out, "\"text\":\"Hello.\""); const char *tool = strstr(out, "\"type\":\"tool_use\""); const char *stop = strstr(out, "event: message_stop"); TEST_ASSERT(msg_start != NULL); TEST_ASSERT(thinking != NULL); TEST_ASSERT(signature != NULL); TEST_ASSERT(text != NULL); TEST_ASSERT(tool != NULL); TEST_ASSERT(stop != NULL); TEST_ASSERT(msg_start < thinking); TEST_ASSERT(thinking < signature); TEST_ASSERT(signature < text); TEST_ASSERT(text < tool); TEST_ASSERT(tool < stop); TEST_ASSERT(strstr(out, DS4_TOOL_CALLS_START) == NULL); free(out); tool_calls_free(&calls); anthropic_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_anthropic_stream_reroutes_second_reasoning_pass(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_ANTHROPIC; r.stream = true; r.think_mode = DS4_THINK_HIGH; r.has_tools = true; anthropic_stream st; TEST_ASSERT(anthropic_sse_start_live(sv[0], &r, "msg_second_think", 5, &st)); const char *partial = "first passescaped draft"; TEST_ASSERT(anthropic_sse_stream_update(sv[0], NULL, &r, "msg_second_think", &st, partial, strlen(partial), false)); const char *complete = "first passescaped draftfinal answer"; TEST_ASSERT(anthropic_sse_finish_live(sv[0], NULL, &r, "msg_second_think", &st, complete, strlen(complete), NULL, "stop", 9)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"thinking\":\"first pass\"") != NULL); TEST_ASSERT(strstr(out, "\"thinking\":\"escaped draft\"") != NULL); TEST_ASSERT(strstr(out, "\"text\":\"final answer\"") != NULL); TEST_ASSERT(strstr(out, "\"text\":\"escaped draft") == NULL); TEST_ASSERT(strstr(out, "") == NULL); free(out); anthropic_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_anthropic_tool_stream_sends_live_tool_use(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_ANTHROPIC; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; r.tool_orders = make_bash_order(); anthropic_stream st; TEST_ASSERT(anthropic_sse_start_live(sv[0], &r, "msg_tool", 7, &st)); const char *raw = "Before.\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo partial"; TEST_ASSERT(anthropic_sse_stream_update(sv[0], NULL, &r, "msg_tool", &st, raw, strlen(raw), false)); const char *raw_complete = "Before.\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo partial done" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; TEST_ASSERT(anthropic_sse_stream_update(sv[0], NULL, &r, "msg_tool", &st, raw_complete, strlen(raw_complete), false)); char *parsed_content = NULL; char *parsed_reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(raw_complete, false, &parsed_content, &parsed_reasoning, &calls)); TEST_ASSERT(calls.len == 1); apply_anthropic_stream_tool_ids(&calls, &st); TEST_ASSERT(calls.v[0].id != NULL); TEST_ASSERT(!strncmp(calls.v[0].id, "toolu_", 6)); TEST_ASSERT(anthropic_sse_finish_live(sv[0], NULL, &r, "msg_tool", &st, raw_complete, strlen(raw_complete), &calls, "tool_calls", 5)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); const char *text = strstr(out, "\"text\":\"Before.\""); const char *tool = strstr(out, "\"type\":\"tool_use\""); const char *key = strstr(out, "\\\"command\\\":\\\""); const char *partial = strstr(out, "\"partial_json\":\"echo partial\""); const char *rest = strstr(out, "\"partial_json\":\" done\""); const char *stop = strstr(out, "event: message_stop"); int tool_use_count = 0; for (const char *p = out; (p = strstr(p, "\"type\":\"tool_use\"")) != NULL; p++) { tool_use_count++; } TEST_ASSERT(text != NULL); TEST_ASSERT(tool != NULL); TEST_ASSERT(key != NULL); TEST_ASSERT(partial != NULL); TEST_ASSERT(rest != NULL); TEST_ASSERT(stop != NULL); TEST_ASSERT(strstr(out, calls.v[0].id) != NULL); TEST_ASSERT(text < tool); TEST_ASSERT(tool < key); TEST_ASSERT(key < partial); TEST_ASSERT(partial < rest); TEST_ASSERT(rest < stop); TEST_ASSERT(tool_use_count == 1); TEST_ASSERT(strstr(out, DS4_TOOL_CALLS_START) == NULL); TEST_ASSERT(strstr(out, DS4_PARAM_START) == NULL); free(out); free(parsed_content); free(parsed_reasoning); tool_calls_free(&calls); anthropic_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_anthropic_usage_reports_cache_details(void) { request r; request_init(&r, REQ_CHAT, 128); r.api = API_ANTHROPIC; r.cache_read_tokens = 7; r.cache_write_tokens = 3; int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) { request_free(&r); return; } TEST_ASSERT(anthropic_final_response(sv[0], false, &r, "msg_usage", "OK", NULL, NULL, "stop", 10, 2)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"usage\":{\"input_tokens\":0") != NULL); TEST_ASSERT(strstr(out, "\"output_tokens\":2") != NULL); TEST_ASSERT(strstr(out, "\"cache_read_input_tokens\":7") != NULL); TEST_ASSERT(strstr(out, "\"cache_creation_input_tokens\":3") != NULL); free(out); close(sv[0]); close(sv[1]); TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) { request_free(&r); return; } anthropic_stream st; TEST_ASSERT(anthropic_sse_start_live(sv[0], &r, "msg_usage_stream", 10, &st)); shutdown(sv[0], SHUT_WR); out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "event: message_start") != NULL); TEST_ASSERT(strstr(out, "\"usage\":{\"input_tokens\":0") != NULL); TEST_ASSERT(strstr(out, "\"output_tokens\":0") != NULL); TEST_ASSERT(strstr(out, "\"cache_read_input_tokens\":7") != NULL); TEST_ASSERT(strstr(out, "\"cache_creation_input_tokens\":3") != NULL); free(out); close(sv[0]); close(sv[1]); request_free(&r); } static void test_openai_tool_stream_sends_incremental_text(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_HIGH; r.has_tools = true; r.tool_orders = make_bash_order(); TEST_ASSERT(sse_chunk(sv[0], &r, "chatcmpl_test", NULL, NULL)); openai_stream st; openai_stream_start(&r, &st); const char *raw1 = "need a toolHello.\n\n"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_test", &st, raw1, strlen(raw1), false)); const char *raw = "need a toolHello.\n\n" DS4_TOOL_CALLS_START "\n"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_test", &st, raw, strlen(raw), false)); tool_calls calls = make_swapped_bash_call(); TEST_ASSERT(openai_sse_finish_live(sv[0], NULL, &r, "chatcmpl_test", &st, raw, strlen(raw), &calls, "tool_calls", 10, 8)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); const char *role = strstr(out, "\"role\":\"assistant\""); const char *thinking = strstr(out, "\"reasoning_content\":\"need a tool\""); const char *text = strstr(out, "\"content\":\"Hello.\""); const char *tool = strstr(out, "\"tool_calls\""); const char *done = strstr(out, "data: [DONE]"); TEST_ASSERT(role != NULL); TEST_ASSERT(thinking != NULL); TEST_ASSERT(text != NULL); TEST_ASSERT(tool != NULL); TEST_ASSERT(done != NULL); TEST_ASSERT(role < thinking); TEST_ASSERT(thinking < text); TEST_ASSERT(text < tool); TEST_ASSERT(tool < done); TEST_ASSERT(strstr(out, DS4_TOOL_CALLS_START) == NULL); TEST_ASSERT(strstr(out, "") == NULL); free(out); tool_calls_free(&calls); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_stream_reroutes_second_reasoning_pass(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_HIGH; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char *partial = "first passescaped draft"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_second_think", &st, partial, strlen(partial), false)); const char *complete = "first passescaped draftfinal answer"; TEST_ASSERT(openai_sse_finish_live(sv[0], NULL, &r, "chatcmpl_second_think", &st, complete, strlen(complete), NULL, "stop", 5, 9)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"reasoning_content\":\"first pass\"") != NULL); TEST_ASSERT(strstr(out, "\"reasoning_content\":\"escaped draft\"") != NULL); TEST_ASSERT(strstr(out, "\"content\":\"final answer\"") != NULL); TEST_ASSERT(strstr(out, "\"content\":\"escaped draft") == NULL); TEST_ASSERT(strstr(out, "") == NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_qwen_tool_stream_sends_answer_before_finish(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.model_syntax = SERVER_MODEL_SYNTAX_QWEN; r.stream = true; r.think_mode = DS4_THINK_HIGH; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char *partial = "first passfirst answer chunk"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_qwen_stream", &st, partial, strlen(partial), false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"reasoning_content\":\"first pass\"") != NULL); TEST_ASSERT(strstr(out, "\"content\":\"first answer chunk\"") != NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_stream_usage_reports_cache_details(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.stream_include_usage = true; r.cache_read_tokens = 7; r.cache_write_tokens = 3; TEST_ASSERT(sse_done(sv[0], &r, "chatcmpl_usage", 10, 2)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"usage\":{\"prompt_tokens\":10") != NULL); TEST_ASSERT(strstr(out, "\"completion_tokens\":2") != NULL); TEST_ASSERT(strstr(out, "\"total_tokens\":12") != NULL); TEST_ASSERT(strstr(out, "\"prompt_tokens_details\":{") != NULL); TEST_ASSERT(strstr(out, "\"cached_tokens\":7") != NULL); TEST_ASSERT(strstr(out, "\"cache_write_tokens\":3") != NULL); TEST_ASSERT(strstr(out, "data: [DONE]") != NULL); free(out); request_free(&r); close(sv[0]); close(sv[1]); } static void test_qwen_stream_split_reasoning_close(void) { const char *raw = "first passfirst answer chunk"; const size_t split = strlen("first pass") == NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_tool_stream_sends_partial_arguments(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; r.tool_orders = make_bash_order(); TEST_ASSERT(sse_chunk(sv[0], &r, "chatcmpl_partial_tool", NULL, NULL)); openai_stream st; openai_stream_start(&r, &st); const char *raw = "Before.\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo partial"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_partial_tool", &st, raw, strlen(raw), false)); const char *raw_complete = "Before.\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo partial done" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_partial_tool", &st, raw_complete, strlen(raw_complete), false)); char *parsed_content = NULL; char *parsed_reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(raw_complete, false, &parsed_content, &parsed_reasoning, &calls)); TEST_ASSERT(calls.len == 1); apply_openai_stream_tool_ids(&calls, &st); TEST_ASSERT(calls.v[0].id != NULL); TEST_ASSERT(!strncmp(calls.v[0].id, "call_", 5)); TEST_ASSERT(openai_sse_finish_live(sv[0], NULL, &r, "chatcmpl_partial_tool", &st, raw_complete, strlen(raw_complete), &calls, "tool_calls", 10, 4)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); const char *text = strstr(out, "\"content\":\"Before.\""); const char *tool = strstr(out, "\"tool_calls\""); const char *key = strstr(out, "\\\"command\\\":\\\""); const char *partial = strstr(out, "\"arguments\":\"echo partial\""); const char *rest = strstr(out, "\"arguments\":\" done\""); int tool_id_count = 0; for (const char *p = out; (p = strstr(p, "\"id\":\"call_")) != NULL; p++) tool_id_count++; TEST_ASSERT(text != NULL); TEST_ASSERT(tool != NULL); TEST_ASSERT(key != NULL); TEST_ASSERT(partial != NULL); TEST_ASSERT(rest != NULL); TEST_ASSERT(strstr(out, calls.v[0].id) != NULL); TEST_ASSERT(text < tool); TEST_ASSERT(tool < partial); TEST_ASSERT(partial < rest); TEST_ASSERT(tool_id_count == 1); TEST_ASSERT(strstr(out, DS4_TOOL_CALLS_START) == NULL); TEST_ASSERT(strstr(out, DS4_PARAM_START) == NULL); free(out); free(parsed_content); free(parsed_reasoning); tool_calls_free(&calls); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_glm_tool_stream_suppresses_raw_tool_call(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; r.model_syntax = SERVER_MODEL_SYNTAX_GLM; r.tool_orders = make_bash_order(); TEST_ASSERT(sse_chunk(sv[0], &r, "chatcmpl_glm_tool", NULL, NULL)); openai_stream st; openai_stream_start(&r, &st); const char *raw = "Before.\n\n" "bash" "commandpwd" ""; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_glm_tool", &st, raw, strlen(raw), false)); char *parsed_content = NULL; char *parsed_reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_GLM, raw, false, &parsed_content, &parsed_reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(openai_sse_finish_live(sv[0], NULL, &r, "chatcmpl_glm_tool", &st, raw, strlen(raw), &calls, "tool_calls", 10, 4)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"content\":\"Before.\"") != NULL); TEST_ASSERT(strstr(out, "\"tool_calls\"") != NULL); TEST_ASSERT(strstr(out, "\"name\":\"bash\"") != NULL); TEST_ASSERT(strstr(out, "\\\"command\\\":") != NULL); TEST_ASSERT(strstr(out, "\\\"pwd\\\"") != NULL); TEST_ASSERT(strstr(out, "") == NULL); TEST_ASSERT(strstr(out, "") == NULL); free(out); free(parsed_content); free(parsed_reasoning); tool_calls_free(&calls); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_tool_stream_waits_for_incomplete_tool_tags(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char *raw_invoke = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_incomplete_tool", &st, raw_invoke, strlen(raw_invoke), false)); TEST_ASSERT(st.mode == OPENAI_STREAM_TOOL); TEST_ASSERT(st.tool.state == DSML_TOOL_BETWEEN_INVOKES); const char *raw_param = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_incomplete_tool", &st, raw_param, strlen(raw_param), false)); TEST_ASSERT(st.mode == OPENAI_STREAM_TOOL); TEST_ASSERT(st.tool.state == DSML_TOOL_BETWEEN_PARAMS); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"name\":\"bash\"") != NULL); TEST_ASSERT(strstr(out, DS4_PARAM_START) == NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_tool_stream_sends_partial_raw_arguments(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char *raw = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"edits\" string=\"false\">[1,2,3"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_raw_tool", &st, raw, strlen(raw), false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"name\":\"edit\"") != NULL); TEST_ASSERT(strstr(out, "\\\"edits\\\":") != NULL); TEST_ASSERT(strstr(out, "\"arguments\":\"[1,2,3\"") != NULL); TEST_ASSERT(strstr(out, DS4_TOOL_CALLS_START) == NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_tool_stream_preserves_literal_entities(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char *raw_partial = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo &"; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_entity_tool", &st, raw_partial, strlen(raw_partial), false)); const char *raw_complete = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo & done" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_entity_tool", &st, raw_complete, strlen(raw_complete), false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"arguments\":\"echo \"") != NULL); TEST_ASSERT(strstr(out, "\"arguments\":\"& done\"") != NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_tool_stream_holds_partial_utf8_arguments(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char prefix[] = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"write\">\n" DS4_PARAM_START " name=\"content\" string=\"true\">flag "; const char suffix[] = " done" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; const char flag_utf8[] = {(char)0xf0, (char)0x9f, (char)0x9a, (char)0xa9, 0}; const char replacement[] = {(char)0xef, (char)0xbf, (char)0xbd, 0}; buf partial = {0}; buf_append(&partial, prefix, strlen(prefix)); buf_putc(&partial, (char)0xf0); buf_putc(&partial, (char)0x9f); TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_utf8_tool", &st, partial.ptr, partial.len, false)); buf complete = {0}; buf_append(&complete, prefix, strlen(prefix)); buf_append(&complete, flag_utf8, 4); buf_append(&complete, suffix, strlen(suffix)); TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_utf8_tool", &st, complete.ptr, complete.len, false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"arguments\":\"flag \"") != NULL); TEST_ASSERT(strstr(out, flag_utf8) != NULL); TEST_ASSERT(strstr(out, replacement) == NULL); free(out); buf_free(&partial); buf_free(&complete); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_openai_tool_stream_handles_multiple_calls(void) { int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; r.has_tools = true; openai_stream st; openai_stream_start(&r, &st); const char *raw = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"read\">\n" DS4_PARAM_START " name=\"path\" string=\"true\">a.c" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">wc -l a.c" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_multi_tool", &st, raw, strlen(raw), false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); int tool_id_count = 0; for (const char *p = out; (p = strstr(p, "\"id\":\"call_")) != NULL; p++) tool_id_count++; TEST_ASSERT(tool_id_count == 2); TEST_ASSERT(strstr(out, "\"name\":\"read\"") != NULL); TEST_ASSERT(strstr(out, "\"name\":\"bash\"") != NULL); TEST_ASSERT(strstr(out, "\\\"path\\\":") != NULL); TEST_ASSERT(strstr(out, "\\\"command\\\":") != NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_streaming_holds_partial_utf8(void) { const char partial[] = {'A', ' ', (char)0xf0, (char)0x9f, 0}; const char complete[] = {'A', ' ', (char)0xf0, (char)0x9f, (char)0x9a, (char)0xa9, ' ', 'd', 'o', 'n', 'e', 0}; const char flag_done[] = {(char)0xf0, (char)0x9f, (char)0x9a, (char)0xa9, ' ', 'd', 'o', 'n', 'e', 0}; const char replacement[] = {(char)0xef, (char)0xbf, (char)0xbd, 0}; TEST_ASSERT(utf8_stream_safe_len(partial, 0, strlen(partial), false) == 2); TEST_ASSERT(utf8_stream_safe_len(complete, 0, strlen(complete), false) == strlen(complete)); int sv[2]; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.stream = true; r.think_mode = DS4_THINK_NONE; openai_stream st; openai_stream_start(&r, &st); TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_utf8", &st, partial, strlen(partial), false)); TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_utf8", &st, complete, strlen(complete), false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); TEST_ASSERT(strstr(out, "\"content\":\"A \"") != NULL); TEST_ASSERT(strstr(out, flag_done) != NULL); TEST_ASSERT(strstr(out, replacement) == NULL); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); } static void test_request_defaults_use_min_p_filtering(void) { request r; request_init(&r, REQ_CHAT, 128); TEST_ASSERT(r.think_mode == DS4_THINK_HIGH); TEST_ASSERT(r.temperature == DS4_DEFAULT_TEMPERATURE); TEST_ASSERT(r.top_p == DS4_DEFAULT_TOP_P); TEST_ASSERT(r.top_k == 0); TEST_ASSERT(r.min_p == DS4_DEFAULT_MIN_P); TEST_ASSERT(!r.ignore_eos); request_free(&r); } static void test_chat_ignore_eos_contract(void) { request r; request_init(&r, REQ_CHAT, 128); char err[128] = {0}; const char *p = "true"; TEST_ASSERT(parse_ignore_eos_value(&p, &r)); TEST_ASSERT(r.ignore_eos); TEST_ASSERT(*p == '\0'); TEST_ASSERT(!request_validate_ignore_eos(&r, err, sizeof(err))); TEST_ASSERT(strstr(err, "temperature") != NULL); r.temperature_set = true; r.temperature = 0.0f; TEST_ASSERT(request_validate_ignore_eos(&r, err, sizeof(err))); p = "false"; TEST_ASSERT(parse_ignore_eos_value(&p, &r)); TEST_ASSERT(!r.ignore_eos); TEST_ASSERT(*p == '\0'); p = "1"; TEST_ASSERT(!parse_ignore_eos_value(&p, &r)); request_free(&r); bool ok = parse_chat_request( NULL, NULL, "{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]," "\"ignore_eos\":true}", 128, 32768, &r, err, sizeof(err)); TEST_ASSERT(!ok); TEST_ASSERT(strstr(err, "temperature") != NULL); ok = parse_chat_request( NULL, NULL, "{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]," "\"temperature\":0,\"ignore_eos\":\"yes\"}", 128, 32768, &r, err, sizeof(err)); TEST_ASSERT(!ok); TEST_ASSERT(!strcmp(err, "invalid JSON request")); } static void test_reasoning_effort_mapping(void) { ds4_think_mode mode = DS4_THINK_NONE; TEST_ASSERT(parse_reasoning_effort_name("low", &mode) && mode == DS4_THINK_LOW); TEST_ASSERT(parse_reasoning_effort_name("minimal", &mode) && mode == DS4_THINK_LOW); TEST_ASSERT(parse_reasoning_effort_name("medium", &mode) && mode == DS4_THINK_MEDIUM); TEST_ASSERT(parse_reasoning_effort_name("high", &mode) && mode == DS4_THINK_HIGH); TEST_ASSERT(parse_reasoning_effort_name("xhigh", &mode) && mode == DS4_THINK_HIGH); TEST_ASSERT(parse_reasoning_effort_name("max", &mode) && mode == DS4_THINK_MAX); TEST_ASSERT(!parse_reasoning_effort_name("banana", &mode)); TEST_ASSERT(think_mode_from_enabled(true, DS4_THINK_LOW) == DS4_THINK_LOW); TEST_ASSERT(think_mode_from_enabled(false, DS4_THINK_LOW) == DS4_THINK_NONE); TEST_ASSERT(!strcmp(ds4_glm_reasoning_effort_text(DS4_THINK_LOW), "Reasoning Effort: High")); TEST_ASSERT(!strcmp(ds4_glm_reasoning_effort_text(DS4_THINK_MEDIUM), "Reasoning Effort: High")); TEST_ASSERT(ds4_think_mode_for_context(DS4_THINK_MAX, 32768) == DS4_THINK_HIGH); TEST_ASSERT(ds4_think_mode_for_context(DS4_THINK_MAX, (int)ds4_think_max_min_context()) == DS4_THINK_MAX); } static void test_model_alias_thinking_controls(void) { TEST_ASSERT(model_alias_disables_thinking("deepseek-chat")); TEST_ASSERT(model_alias_disables_thinking("glm-5.2-chat")); TEST_ASSERT(model_alias_disables_thinking("glm-5.2-no-think")); TEST_ASSERT(model_alias_disables_thinking("zai/glm-5.2-chat")); TEST_ASSERT(!model_alias_disables_thinking("glm-5.2")); TEST_ASSERT(model_alias_enables_thinking("deepseek-reasoner")); TEST_ASSERT(model_alias_enables_thinking("glm-5.2-reasoner")); TEST_ASSERT(model_alias_enables_thinking("zai/glm-5.2-reasoner")); TEST_ASSERT(server_model_alias_known("glm-5.2-chat")); TEST_ASSERT(server_model_alias_known("glm-5.2-reasoner")); TEST_ASSERT(model_alias_disables_thinking("glm-5.3-flash-chat")); TEST_ASSERT(model_alias_disables_thinking("zai/glm-5.3-flash-chat")); TEST_ASSERT(model_alias_enables_thinking("glm-5.3-flash-reasoner")); TEST_ASSERT(model_alias_enables_thinking("zai/glm-5.3-flash-reasoner")); TEST_ASSERT(server_model_alias_known("glm-5.3-flash")); TEST_ASSERT(server_model_alias_known("glm-5.3-flash-chat")); TEST_ASSERT(server_model_alias_known("glm-5.3-flash-reasoner")); } static void test_api_thinking_controls_parse(void) { bool enabled = true; const char *thinking = "{\"type\":\"disabled\",\"budget_tokens\":1024}"; TEST_ASSERT(parse_thinking_control_value(&thinking, &enabled)); TEST_ASSERT(!enabled); thinking = "true"; TEST_ASSERT(parse_thinking_control_value(&thinking, &enabled)); TEST_ASSERT(enabled); ds4_think_mode mode = DS4_THINK_HIGH; const char *anth_effort = "{\"effort\":\"max\",\"other\":true}"; TEST_ASSERT(parse_output_config_effort(&anth_effort, &mode)); TEST_ASSERT(mode == DS4_THINK_MAX); const char *openai_effort = "\"xhigh\""; mode = DS4_THINK_HIGH; TEST_ASSERT(parse_reasoning_effort_value(&openai_effort, &mode)); TEST_ASSERT(mode == DS4_THINK_HIGH); const int levels[] = {0, 1, 25, 50, 99, 100}; for (size_t i = 0; i < sizeof(levels) / sizeof(levels[0]); i++) { char value[16]; snprintf(value, sizeof(value), "\"%d\"", levels[i]); const char *p = value; mode = DS4_THINK_NONE; TEST_ASSERT(parse_reasoning_effort_value(&p, &mode) && *p == '\0'); TEST_ASSERT(ds4_think_mode_level(mode) == levels[i]); } const char *invalid[] = {"\"\"", "\"-1\"", "\"101\"", "\"1.5\"", "\"50x\"", "\" 50\"", "\"999999999999999999999\""}; for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { const char *p = invalid[i]; mode = DS4_THINK_MEDIUM; TEST_ASSERT(!parse_reasoning_effort_value(&p, &mode)); TEST_ASSERT(mode == DS4_THINK_MEDIUM); } } static void test_render_think_max_prompt_prefix(void) { chat_msgs msgs = {0}; chat_msg sys = {0}; sys.role = xstrdup("system"); sys.content = xstrdup("You are terse."); chat_msgs_push(&msgs, sys); chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("Hello"); chat_msgs_push(&msgs, user); char *prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_MAX); TEST_ASSERT(prompt != NULL); TEST_ASSERT(!strncmp(prompt, "<|begin▁of▁sentence|>", strlen("<|begin▁of▁sentence|>"))); TEST_ASSERT(strstr(prompt, ds4_think_max_prefix()) != NULL); TEST_ASSERT(strstr(prompt, "You are terse.<|User|>Hello<|Assistant|>") != NULL); TEST_ASSERT(strstr(prompt, "") == NULL); free(prompt); chat_msgs_free(&msgs); } static void test_render_non_thinking_prompt_closes_think(void) { chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("Hello"); chat_msgs_push(&msgs, user); char *prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_NONE); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, ds4_think_max_prefix()) == NULL); TEST_ASSERT(strstr(prompt, "<|User|>Hello<|Assistant|>") != NULL); free(prompt); chat_msgs_free(&msgs); } static void test_render_drops_old_reasoning_without_tools(void) { chat_msgs msgs = {0}; chat_msg user1 = {0}; user1.role = xstrdup("user"); user1.content = xstrdup("first"); chat_msgs_push(&msgs, user1); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup("old hidden reasoning"); assistant.content = xstrdup("first answer"); chat_msgs_push(&msgs, assistant); chat_msg user2 = {0}; user2.role = xstrdup("user"); user2.content = xstrdup("second"); chat_msgs_push(&msgs, user2); char *prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "old hidden reasoning") == NULL); TEST_ASSERT(strstr(prompt, "<|Assistant|>first answer") != NULL); TEST_ASSERT(strstr(prompt, "<|User|>second<|Assistant|>") != NULL); free(prompt); chat_msgs_free(&msgs); } static void test_render_preserves_reasoning_with_tools(void) { chat_msgs msgs = {0}; chat_msg user1 = {0}; user1.role = xstrdup("user"); user1.content = xstrdup("first"); chat_msgs_push(&msgs, user1); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup("tool reasoning"); assistant.content = xstrdup(""); tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"pwd\"}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.content = xstrdup("/tmp"); chat_msgs_push(&msgs, tool); char *prompt = render_chat_prompt_text(&msgs, "{}", NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "tool reasoning") != NULL); TEST_ASSERT(strstr(prompt, "/tmp") != NULL); free(prompt); prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "tool reasoning") != NULL); TEST_ASSERT(strstr(prompt, "/tmp") != NULL); free(prompt); chat_msgs_free(&msgs); } static void test_render_chat_prompt_text_renders_tools_before_system(void) { /* The tool-schema block must sit at the head of the system region so the * client's system content stays at the tail, right before <|User|>. * That keeps a per-request dynamic tail (e.g. a timestamp) out of the * cached prefix without losing the tool schemas to the trim. */ chat_msgs msgs = {0}; chat_msg sys = {0}; sys.role = xstrdup("system"); sys.content = xstrdup("CLIENT_SYSTEM_MARKER"); chat_msgs_push(&msgs, sys); chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("hello"); chat_msgs_push(&msgs, user); char *prompt = render_chat_prompt_text(&msgs, "TOOL_SCHEMA_MARKER", NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); const char *tools = strstr(prompt, "## Tools"); const char *client = strstr(prompt, "CLIENT_SYSTEM_MARKER"); const char *user_m = strstr(prompt, "<|User|>"); TEST_ASSERT(tools && client && user_m); TEST_ASSERT(tools < client); TEST_ASSERT(client < user_m); free(prompt); chat_msgs_free(&msgs); } static void test_render_qwen_chat_prompt_text(void) { chat_msgs msgs = {0}; chat_msg sys = {0}; sys.role = xstrdup("system"); sys.content = xstrdup("You are terse."); chat_msgs_push(&msgs, sys); chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("Hello"); chat_msgs_push(&msgs, user); tool_schema_orders orders = make_bash_order(); const char *tool_schemas = "{\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"command\":{}}},\"strict\":true}"; char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, tool_schemas, &orders, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(!strncmp(prompt, "<|im_start|>system\nReasoning effort is set to xhigh.", 52)); TEST_ASSERT(strstr(prompt, "\n\n# Tools\n\nYou have access to the following functions:\n\n\n" "{\"type\": \"function\", \"function\": {\"name\": \"bash\", \"parameters\": " "{\"type\":\"object\",\"properties\":{\"command\":{}}}}}\n\n\n" "If you choose to call a function ONLY reply in the following format with NO suffix:") != NULL); TEST_ASSERT(strstr(prompt, "\n\nYou are terse.<|im_end|>\n" "<|im_start|>user\nHello<|im_end|>\n" "<|im_start|>assistant\n\n") != NULL); TEST_ASSERT(prompt[strlen(prompt) - 1] == '\n'); free(prompt); /* no tools, thinking off: bare system + user, empty think block */ prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, NULL, DS4_THINK_NONE); TEST_ASSERT(prompt != NULL); TEST_ASSERT(!strcmp(prompt, "<|im_start|>system\nYou are terse.<|im_end|>\n" "<|im_start|>user\nHello<|im_end|>\n" "<|im_start|>assistant\n\n\n\n\n")); free(prompt); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); } static void test_qwen_reasoning_effort_levels(void) { bool enabled = true, got = false; ds4_think_mode mode = DS4_THINK_HIGH; const char *kwargs = "{\"enable_thinking\": false, \"reasoning_effort\": \"low\", \"preserve_thinking\": true}"; TEST_ASSERT(parse_chat_template_kwargs(&kwargs, &enabled, &got, &mode)); TEST_ASSERT(!enabled && got && mode == DS4_THINK_LOW); kwargs = "null"; TEST_ASSERT(parse_chat_template_kwargs(&kwargs, &enabled, &got, &mode)); TEST_ASSERT(!enabled && mode == DS4_THINK_LOW); chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("Hello"); chat_msgs_push(&msgs, user); char *low = render_chat_prompt_text_for_syntax(SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, NULL, DS4_THINK_LOW); TEST_ASSERT(low && strstr(low, "<|im_start|>system\nReasoning effort is set to low. Keep your thinking brief") == low); TEST_ASSERT(strstr(low, "<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n\n") != NULL); char *medium = render_chat_prompt_text_for_syntax(SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, NULL, DS4_THINK_MEDIUM); TEST_ASSERT(medium && strstr(medium, "<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n\n") != NULL); TEST_ASSERT(strstr(medium, "<|im_start|>system") == NULL && strstr(medium, "Reasoning effort") == NULL); char *max = render_chat_prompt_text_for_syntax(SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, NULL, DS4_THINK_MAX); TEST_ASSERT(max && !strncmp(max, "<|im_start|>system\nReasoning effort is set to xhigh.", 52)); free(low); free(medium); free(max); chat_msgs_free(&msgs); } static void test_qwen_tool_visible_checkpoint_boundary(void) { for (int thinking = 0; thinking < 2; thinking++) { for (int with_content = 0; with_content < 2; with_content++) { chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("run it"); chat_msgs_push(&msgs, user); request r = {0}; r.kind = REQ_CHAT; r.model_syntax = SERVER_MODEL_SYNTAX_QWEN; r.think_mode = thinking ? DS4_THINK_HIGH : DS4_THINK_NONE; r.prompt_text = render_qwen_chat_prompt_text(&msgs, NULL, NULL, r.think_mode); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.content = xstrdup(with_content ? "Running." : ""); tool_call call = {0}; call.name = xstrdup("bash"); call.arguments = xstrdup("{}"); tool_calls_push(&assistant.calls, call); assistant.calls.raw_tool_text = xstrdup( "\n\n\n\n\n"); char *visible = build_qwen_tool_turn_visible_text( &r, "tool_calls", false, assistant.content, &assistant.calls); TEST_ASSERT(visible != NULL); TEST_ASSERT(build_qwen_tool_turn_visible_text( &r, "length", false, assistant.content, &assistant.calls) == NULL); TEST_ASSERT(build_qwen_tool_turn_visible_text( &r, "tool_calls", true, assistant.content, &assistant.calls) == NULL); r.image_count = 1; TEST_ASSERT(build_qwen_tool_turn_visible_text( &r, "tool_calls", false, assistant.content, &assistant.calls) == NULL); r.image_count = 0; r.api = API_RESPONSES; TEST_ASSERT(build_qwen_tool_turn_visible_text( &r, "tool_calls", false, assistant.content, &assistant.calls) == NULL); chat_msgs_push(&msgs, assistant); chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.content = xstrdup("ok"); chat_msgs_push(&msgs, tool); char *next = render_qwen_chat_prompt_text(&msgs, NULL, NULL, r.think_mode); TEST_ASSERT(visible && !strncmp(next, visible, strlen(visible))); if (visible && !strncmp(next, visible, strlen(visible))) { /* The live frontier ends at the sampled tool block. The new * suffix must supply exactly the missing assistant boundary. */ const char *tail = next + strlen(visible); const char *boundary = "<|im_end|>\n<|im_start|>user\n"; TEST_ASSERT(!strncmp(tail, boundary, strlen(boundary))); } if (thinking) { msgs.v[1].reasoning = xstrdup("hidden reasoning"); char *with_reasoning = render_qwen_chat_prompt_text(&msgs, NULL, NULL, r.think_mode); TEST_ASSERT(visible && strncmp(with_reasoning, visible, strlen(visible)) != 0); free(with_reasoning); } free(next); free(visible); free(r.prompt_text); chat_msgs_free(&msgs); } } } static void test_render_qwen_tool_round_trip(void) { chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("run it"); chat_msgs_push(&msgs, user); chat_msg asst = {0}; asst.role = xstrdup("assistant"); asst.content = xstrdup("Running."); asst.reasoning = xstrdup("need bash"); tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\": \"echo hi\", \"timeout\": 10}"); tool_calls_push(&asst.calls, tc); chat_msgs_push(&msgs, asst); chat_msg first = {0}; first.role = xstrdup("tool"); first.content = xstrdup("hi"); chat_msgs_push(&msgs, first); chat_msg second = {0}; second.role = xstrdup("tool"); second.content = xstrdup("second"); chat_msgs_push(&msgs, second); tool_schema_orders orders = make_bash_order(); char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, &orders, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); const char *expected_tail = "<|im_start|>user\nrun it<|im_end|>\n" "<|im_start|>assistant\n\nneed bash\n\n\nRunning.\n\n" "\n\n\necho hi\n\n" "\n10\n\n\n<|im_end|>\n" "<|im_start|>user\n\nhi\n\n\nsecond\n<|im_end|>\n" "<|im_start|>assistant\n\n"; TEST_ASSERT(strstr(prompt, expected_tail) != NULL); free(prompt); char *tail = render_live_tool_tail_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, 2, &orders, DS4_THINK_NONE); TEST_ASSERT(tail != NULL); TEST_ASSERT(!strcmp(tail, "<|im_end|>\n<|im_start|>user\n\nhi\n\n\nsecond\n<|im_end|>\n" "<|im_start|>assistant\n\n\n\n\n")); free(tail); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); } static void test_parse_qwen_tool_call_message(void) { const char *generated = "need bash\n\n\nOK\n\n" "\n\n" "\necho hi\nsecond line & < </parameter> &lt;/parameter>\n\n" "\n10\n\n" "\n{\"a\": [1, 2]}\n\n" "\n\n" "\n\n\n/tmp/x\n\n\n"; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, generated, true, &content, &reasoning, &calls)); TEST_ASSERT(reasoning && !strcmp(reasoning, "need bash")); TEST_ASSERT(content && !strcmp(content, "OK")); TEST_ASSERT(calls.len == 2); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "bash")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"command\": \"echo hi\\nsecond line & < </parameter>\"") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"timeout\": 10") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"opts\": {\"a\":[1,2]}") != NULL); TEST_ASSERT(calls.v[1].name && !strcmp(calls.v[1].name, "read")); TEST_ASSERT(strstr(calls.v[1].arguments, "\"path\": \"/tmp/x\"") != NULL); TEST_ASSERT(calls.raw_tool_text && !strncmp(calls.raw_tool_text, "\n\n\n", 29)); free(content); free(reasoning); tool_calls_free(&calls); /* thinking never closed: no tool call is executed */ content = NULL; reasoning = NULL; tool_calls none = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, "still thinking about ", true, &content, &reasoning, &none)); TEST_ASSERT(none.len == 0); TEST_ASSERT(content && !strcmp(content, "")); free(content); free(reasoning); tool_calls_free(&none); /* malformed: missing */ content = NULL; reasoning = NULL; tool_calls bad = {0}; TEST_ASSERT(!parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, "x\n\n\n\nls\n\n", true, &content, &reasoning, &bad)); free(content); free(reasoning); tool_calls_free(&bad); } static void test_qwen_literal_tool_end_in_argument(void) { const char *block = "\n\n\n" "literal and and with ‘quotes’ and “quotes”\n" "\n\n/tmp/markers.txt\n\n" "\n\n" "\n\n\n/tmp/markers.txt\n" "\n\n"; for (int thinking = 0; thinking < 2; thinking++) { buf generated = {0}; if (thinking) buf_puts(&generated, "Writing a marker example.\n\n"); buf_puts(&generated, block); char *content = NULL, *reasoning = NULL; tool_calls calls = {0}; const char *finish = "stop"; char err[128] = {0}; bool recovered = false; TEST_ASSERT(parse_generated_message_for_response_for_syntax( SERVER_MODEL_SYNTAX_QWEN, generated.ptr, true, true, thinking, &finish, err, sizeof(err), &content, &reasoning, &calls, &recovered, NULL)); TEST_ASSERT(!recovered && !err[0]); TEST_ASSERT(calls.len == 2); if (calls.len != 2) { free(content); free(reasoning); tool_calls_free(&calls); buf_free(&generated); continue; } TEST_ASSERT(!strcmp(calls.v[0].name, "write")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"content\": \"literal and and with ‘quotes’ and “quotes”\"") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"path\": \"/tmp/markers.txt\"") != NULL); TEST_ASSERT(!strcmp(calls.v[1].name, "read")); TEST_ASSERT(calls.raw_tool_text && strstr(calls.raw_tool_text, block)); free(content); free(reasoning); tool_calls_free(&calls); buf_free(&generated); } /* Consuming values first must not accept missing structural delimiters. */ const char *bad[] = { "", "", "literal ", "literal ", "literal ", }; for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { char *content = NULL, *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(!parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, bad[i], false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 0); free(content); free(reasoning); tool_calls_free(&calls); } } static void test_qwen_string_arguments_follow_schema(void) { tool_schema_orders orders = {0}; tool_schema_orders_add_json(&orders, "{\"name\":\"write\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"content\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}}}}"); const char *values[] = {"42", "false", "null", "{\n \"value\": 42\n}", "[1, 2]"}; for (size_t i = 0; i < sizeof(values) / sizeof(*values); i++) { buf raw = {0}; buf_printf(&raw, "\n%s\n" "\n42\n", values[i]); const char *finish = "stop"; char err[128] = {0}; char *content = NULL, *reasoning = NULL; tool_calls calls = {0}; bool recovered = false; TEST_ASSERT(parse_generated_message_for_response_for_syntax( SERVER_MODEL_SYNTAX_QWEN, raw.ptr, true, true, false, &finish, err, sizeof(err), &content, &reasoning, &calls, &recovered, &orders)); TEST_ASSERT(!recovered && calls.len == 1); if (calls.len == 1) { json_args args = {0}; TEST_ASSERT(json_args_parse(calls.v[0].arguments, &args)); TEST_ASSERT(args.len == 2); if (args.len == 2) { TEST_ASSERT(args.v[0].is_string && !strcmp(args.v[0].value, values[i])); TEST_ASSERT(!args.v[1].is_string && !strcmp(args.v[1].value, "42")); } json_args_free(&args); } tool_calls_free(&calls); free(content); free(reasoning); buf_free(&raw); } tool_schema_orders_free(&orders); } /* Qwen: the thinking-mode live checkpoint key must be exactly what the next * request renders for this turn when the client does not send reasoning back */ static void test_qwen_thinking_visible_text_matches_render(void) { const char *schemas[] = {NULL, "{\"name\":\"lookup\",\"parameters\":{\"type\":\"object\"}}"}; for (size_t i = 0; i < sizeof(schemas) / sizeof(schemas[0]); i++) { chat_msgs msgs = {0}; chat_msg user1 = {0}; user1.role = xstrdup("user"); user1.content = xstrdup("What is 2+2?"); chat_msgs_push(&msgs, user1); char *prompt_text = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, schemas[i], NULL, DS4_THINK_HIGH); const char *gen = "<|im_start|>assistant\n\n"; TEST_ASSERT(strlen(prompt_text) > strlen(gen)); TEST_ASSERT(!strcmp(prompt_text + strlen(prompt_text) - strlen(gen), gen)); request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; r.model_syntax = SERVER_MODEL_SYNTAX_QWEN; r.prompt_text = xstrdup(prompt_text); char *visible = build_thinking_visible_text(&r, "The answer is 4.\n"); TEST_ASSERT(visible != NULL); chat_msgs history = {0}; chat_msg h_user1 = {0}; h_user1.role = xstrdup("user"); h_user1.content = xstrdup("What is 2+2?"); chat_msgs_push(&history, h_user1); chat_msg h_asst = {0}; h_asst.role = xstrdup("assistant"); h_asst.content = xstrdup("The answer is 4."); chat_msgs_push(&history, h_asst); chat_msg h_user2 = {0}; h_user2.role = xstrdup("user"); h_user2.content = xstrdup("Thanks!"); chat_msgs_push(&history, h_user2); char *future = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &history, schemas[i], NULL, DS4_THINK_HIGH); TEST_ASSERT(strlen(future) > strlen(visible)); TEST_ASSERT(!memcmp(future, visible, strlen(visible))); TEST_ASSERT(!strncmp(future + strlen(visible), "<|im_start|>user\n", 17)); free(future); /* Echoed or edited history must not match the reasoning-free alias. */ history.v[1].reasoning = xstrdup("2 plus 2 is 4."); future = render_qwen_chat_prompt_text(&history, schemas[i], NULL, DS4_THINK_HIGH); TEST_ASSERT(strncmp(future, visible, strlen(visible)) != 0); free(future); free(history.v[1].reasoning); history.v[1].reasoning = NULL; free(history.v[1].content); history.v[1].content = xstrdup("An edited answer."); future = render_qwen_chat_prompt_text(&history, schemas[i], NULL, DS4_THINK_HIGH); TEST_ASSERT(strncmp(future, visible, strlen(visible)) != 0); free(future); free(history.v[1].content); history.v[1].content = xstrdup("The answer is 4."); future = render_qwen_chat_prompt_text(&history, "{\"name\":\"different_tool\"}", NULL, DS4_THINK_HIGH); TEST_ASSERT(strncmp(future, visible, strlen(visible)) != 0); free(future); free(visible); request_free(&r); free(prompt_text); chat_msgs_free(&history); chat_msgs_free(&msgs); } } static void test_qwen_checkpoint_suffix_matches_render(void) { /* prompt_text + checkpoint suffix must equal what the next request renders */ request r; request_init(&r, REQ_CHAT, 64); r.model_syntax = SERVER_MODEL_SYNTAX_QWEN; r.think_mode = DS4_THINK_HIGH; chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("run it"); chat_msgs_push(&msgs, user); char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, NULL, DS4_THINK_HIGH); tool_calls calls = {0}; tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\": \"ls\"}"); tool_calls_push(&calls, tc); char *suffix = build_tool_checkpoint_suffix(&r, "", "think first", &calls); buf live = {0}; buf_puts(&live, prompt); buf_puts(&live, suffix); chat_msg asst = {0}; asst.role = xstrdup("assistant"); asst.content = xstrdup(""); asst.reasoning = xstrdup("think first"); tool_call tc2 = {0}; tc2.name = xstrdup("bash"); tc2.arguments = xstrdup("{\"command\": \"ls\"}"); tool_calls_push(&asst.calls, tc2); chat_msgs_push(&msgs, asst); chat_msg res = {0}; res.role = xstrdup("tool"); res.content = xstrdup("a.txt"); chat_msgs_push(&msgs, res); char *next = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_QWEN, &msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(next && live.ptr && !strncmp(next, live.ptr, live.len)); TEST_ASSERT(!strcmp(next + live.len, "\n<|im_start|>user\n\na.txt\n<|im_end|>\n" "<|im_start|>assistant\n\n")); buf_free(&live); free(next); free(suffix); free(prompt); tool_calls_free(&calls); chat_msgs_free(&msgs); request_free(&r); } static void test_qwen_tool_checkpoint_round_trip(void) { /* parse -> render must reproduce the model's raw tool text */ const char *raw = "\n\n\n\n\necho hi\n\n\n"; char *generated = NULL; { buf b = {0}; buf_puts(&b, "done\n\nOK"); buf_puts(&b, raw); generated = buf_take(&b); } char *content = NULL, *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, generated, true, &content, &reasoning, &calls)); buf out = {0}; buf_puts(&out, "OK"); append_tool_calls_text_for_syntax(&out, SERVER_MODEL_SYNTAX_QWEN, &calls, NULL); TEST_ASSERT(out.ptr && !strcmp(out.ptr + 2, raw)); buf_free(&out); free(content); free(reasoning); free(generated); tool_calls_free(&calls); } static void test_qwen_sampled_tool_text_after_think_renders_exactly(void) { /* the model's own whitespace between and its tool call is * replayed once, not stacked on the template separator */ const char *block = "\n\n\nTokyo\n\n\n"; buf generated = {0}; buf_puts(&generated, "Need Tokyo.\n\n\n\n\n"); buf_puts(&generated, block); chat_msg asst = {0}; asst.role = xstrdup("assistant"); TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, generated.ptr, true, &asst.content, &asst.reasoning, &asst.calls)); buf expected = {0}; buf_puts(&expected, "<|im_start|>assistant\n\nNeed Tokyo.\n\n\n\n\n"); buf_puts(&expected, block); buf_puts(&expected, "<|im_end|>\n"); buf out = {0}; append_qwen_assistant_message(&out, &asst, NULL); TEST_ASSERT(out.ptr && !strcmp(out.ptr, expected.ptr)); buf_free(&out); buf_free(&expected); buf_free(&generated); chat_msg_free(&asst); /* no-thinking turn: the empty think block came from the prompt and stays */ chat_msg plain = {0}; plain.role = xstrdup("assistant"); TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, block, false, &plain.content, &plain.reasoning, &plain.calls)); buf_puts(&expected, "<|im_start|>assistant\n\n\n\n\n"); buf_puts(&expected, block); buf_puts(&expected, "<|im_end|>\n"); append_qwen_assistant_message(&out, &plain, NULL); TEST_ASSERT(out.ptr && !strcmp(out.ptr, expected.ptr)); buf_free(&out); buf_free(&expected); chat_msg_free(&plain); } static void test_qwen_parallel_tool_calls_parse_and_replay(void) { /* several blocks in one turn: every call parsed in order, the whole span replayed */ const char *generated = "\n\n\nOslo\n\n\n\n" "\n\n\nLisbon\n\n\n\n" "\n\n\n24 - 9\n\n\n"; char *content = NULL, *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_QWEN, generated, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 3); TEST_ASSERT(!strcmp(calls.v[0].name, "get_weather") && !strcmp(calls.v[0].arguments, "{\"city\": \"Oslo\"}")); TEST_ASSERT(!strcmp(calls.v[1].name, "get_weather") && !strcmp(calls.v[1].arguments, "{\"city\": \"Lisbon\"}")); TEST_ASSERT(!strcmp(calls.v[2].name, "calc") && !strcmp(calls.v[2].arguments, "{\"expression\": \"24 - 9\"}")); TEST_ASSERT(!content || !content[0]); buf out = {0}; append_tool_calls_text_for_syntax(&out, SERVER_MODEL_SYNTAX_QWEN, &calls, NULL); TEST_ASSERT(out.ptr && !strcmp(out.ptr, generated)); buf_free(&out); free(content); free(reasoning); tool_calls_free(&calls); } static void test_qwen_plain_answer_keeps_trailing_whitespace(void) { chat_msg plain = {0}; plain.role = xstrdup("assistant"); plain.content = xstrdup("Done.\n"); buf out = {0}; append_qwen_assistant_message(&out, &plain, NULL); TEST_ASSERT(out.ptr && !strcmp(out.ptr, "<|im_start|>assistant\n\n\n\n\nDone.\n<|im_end|>\n")); buf_free(&out); chat_msg_free(&plain); chat_msg with_call = {0}; with_call.role = xstrdup("assistant"); with_call.content = xstrdup("Look:\n"); tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\": \"ls\"}"); tool_calls_push(&with_call.calls, tc); append_qwen_assistant_message(&out, &with_call, NULL); TEST_ASSERT(out.ptr && !strcmp(out.ptr, "<|im_start|>assistant\n\n\n\n\nLook:\n\n\n\n" "\nls\n\n\n<|im_end|>\n")); buf_free(&out); chat_msg_free(&with_call); } static void test_render_glm_chat_prompt_text(void) { chat_msgs msgs = {0}; chat_msg sys = {0}; sys.role = xstrdup("system"); sys.content = xstrdup("You are terse."); chat_msgs_push(&msgs, sys); chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("Hello"); chat_msgs_push(&msgs, user); tool_schema_orders orders = make_bash_order(); const char *tool_schemas = "{\"name\":\"hidden\",\"defer_loading\":true,\"parameters\":{}}\n" "{\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"command\":{}}},\"strict\":true}"; char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &msgs, tool_schemas, &orders, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); const char *expected = "[gMASK]<|system|>Reasoning Effort: High<|system|>\n" "# Tools\n\n" "You may call one or more functions to assist with the user query.\n\n" "You are provided with function signatures within XML tags:\n" "\n\n" "{\"name\": \"bash\", \"parameters\": {\"type\":\"object\",\"properties\":{\"command\":{}}}}\n\n\n" "\n\n" "For each function call, output the function name and arguments within the following XML format:\n" "{function-name}{arg-key-1}" "{arg-value-1}{arg-key-2}" "{arg-value-2}..." "<|system|>You are terse.<|user|>Hello<|assistant|>"; TEST_ASSERT(!strcmp(prompt, expected)); free(prompt); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); } static void test_render_glm_drops_old_reasoning_without_tools(void) { chat_msgs msgs = {0}; chat_msg user1 = {0}; user1.role = xstrdup("user"); user1.content = xstrdup("first"); chat_msgs_push(&msgs, user1); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup("old hidden reasoning"); assistant.content = xstrdup("first answer"); chat_msgs_push(&msgs, assistant); chat_msg user2 = {0}; user2.role = xstrdup("user"); user2.content = xstrdup("second"); chat_msgs_push(&msgs, user2); char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "old hidden reasoning") == NULL); TEST_ASSERT(strstr(prompt, "<|assistant|>first answer") != NULL); TEST_ASSERT(strstr(prompt, "<|user|>second<|assistant|>") != NULL); free(prompt); chat_msgs_free(&msgs); } static void test_render_glm_preserves_reasoning_with_tools(void) { chat_msgs msgs = {0}; chat_msg user1 = {0}; user1.role = xstrdup("user"); user1.content = xstrdup("first"); chat_msgs_push(&msgs, user1); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup("tool reasoning"); assistant.content = xstrdup(""); tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"pwd\"}"); tool_calls_push(&assistant.calls, tc); tool_call tc2 = {0}; tc2.name = xstrdup("bash"); tc2.arguments = xstrdup("{\"command\":\"ls\"}"); tool_calls_push(&assistant.calls, tc2); chat_msgs_push(&msgs, assistant); chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.content = xstrdup("/tmp"); chat_msgs_push(&msgs, tool); tool_schema_orders orders = make_bash_order(); char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &msgs, NULL, &orders, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); const char *expected = "[gMASK]<|system|>Reasoning Effort: High" "<|user|>first<|assistant|>tool reasoning" "bashcommandpwd" "bashcommand" "ls" "<|observation|>/tmp" "<|assistant|>"; TEST_ASSERT(!strcmp(prompt, expected)); free(prompt); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); } static void test_glm_tool_calls_render_matches_template(void) { const char *tools = "[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"," "\"parameters\":{\"type\":\"object\",\"properties\":" "{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}]"; const char *raw_block = "get_weathercityParis"; chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("What is the weather in Paris?"); chat_msgs_push(&msgs, user); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.content = xstrdup(""); tool_call call = {0}; call.name = xstrdup("get_weather"); call.arguments = xstrdup("{\"city\":\"Paris\"}"); tool_calls_push(&assistant.calls, call); chat_msgs_push(&msgs, assistant); chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.content = xstrdup("18C, light rain."); chat_msgs_push(&msgs, tool); char *canonical = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &msgs, tools, NULL, DS4_THINK_HIGH); TEST_ASSERT(canonical != NULL); TEST_ASSERT(strstr(canonical, raw_block) != NULL); TEST_ASSERT(strstr(canonical, "") != NULL); TEST_ASSERT(strstr(canonical, "<|observation|>") != NULL); TEST_ASSERT(strstr(canonical, "\n") == NULL); TEST_ASSERT(strstr(canonical, "\n") == NULL); /* The live path splices the sampled block; both renders must be identical, * which is what lets a restart replay match the visible key. */ msgs.v[1].calls.raw_tool_text = xstrdup(raw_block); char *raw = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &msgs, tools, NULL, DS4_THINK_HIGH); TEST_ASSERT(raw != NULL); TEST_ASSERT(!strcmp(raw, canonical)); free(raw); free(canonical); chat_msgs_free(&msgs); } static void test_render_glm_groups_tool_results(void) { chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("inspect both"); chat_msgs_push(&msgs, user); chat_msg first = {0}; first.role = xstrdup("tool"); first.content = xstrdup("first result"); chat_msgs_push(&msgs, first); chat_msg second = {0}; second.role = xstrdup("tool"); second.content = xstrdup("second result"); chat_msgs_push(&msgs, second); char *prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); const char *observation = strstr(prompt, "<|observation|>"); TEST_ASSERT(observation != NULL); TEST_ASSERT(strstr(observation + 1, "<|observation|>") == NULL); TEST_ASSERT(strstr(prompt, "<|observation|>first result" "second result" "<|assistant|>") != NULL); free(prompt); chat_msgs_free(&msgs); chat_msgs anthropic = {0}; chat_msg wrapped = {0}; wrapped.role = xstrdup("user"); wrapped.content = xstrdup( "onetwo"); chat_msg_add_tool_call_id(&wrapped, "call_1"); chat_msg_add_tool_call_id(&wrapped, "call_2"); chat_msgs_push(&anthropic, wrapped); prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &anthropic, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "<|observation|>one" "two<|assistant|>") != NULL); TEST_ASSERT(strstr(prompt, "<|user|>") == NULL); free(prompt); chat_msgs_free(&anthropic); } static void test_dsml_tool_args_preserve_call_order(void) { tool_calls calls = make_swapped_bash_call(); buf b = {0}; append_dsml_tool_calls_text(&b, &calls, false); const char *command = strstr(b.ptr, "name=\"command\""); const char *description = strstr(b.ptr, "name=\"description\""); const char *timeout = strstr(b.ptr, "name=\"timeout\""); TEST_ASSERT(command != NULL); TEST_ASSERT(description != NULL); TEST_ASSERT(timeout != NULL); TEST_ASSERT(description < command); TEST_ASSERT(command < timeout); buf_free(&b); tool_calls_free(&calls); } static void test_openai_tool_args_preserve_call_order(void) { request r; request_init(&r, REQ_CHAT, 128); r.tool_orders = make_bash_order(); tool_calls calls = make_swapped_bash_call(); buf b = {0}; append_tool_calls_json(&b, &calls, "test", &r.tool_orders); const char *command = strstr(b.ptr, "\\\"command\\\""); const char *description = strstr(b.ptr, "\\\"description\\\""); const char *timeout = strstr(b.ptr, "\\\"timeout\\\""); TEST_ASSERT(command != NULL); TEST_ASSERT(description != NULL); TEST_ASSERT(timeout != NULL); TEST_ASSERT(description < command); TEST_ASSERT(command < timeout); buf_free(&b); tool_calls_free(&calls); request_free(&r); } static void test_anthropic_thinking_and_tool_args_preserve_call_order(void) { request r; request_init(&r, REQ_CHAT, 128); r.tool_orders = make_bash_order(); tool_calls calls = make_swapped_bash_call(); buf b = {0}; append_anthropic_content(&b, "done", "thinking text", &calls, "msg_1", &r.tool_orders); const char *thinking = strstr(b.ptr, "\"type\":\"thinking\""); const char *text = strstr(b.ptr, "\"type\":\"text\""); const char *tool = strstr(b.ptr, "\"type\":\"tool_use\""); const char *command = strstr(b.ptr, "\"command\""); const char *description = strstr(b.ptr, "\"description\""); TEST_ASSERT(thinking != NULL); TEST_ASSERT(text != NULL); TEST_ASSERT(tool != NULL); TEST_ASSERT(thinking < text); TEST_ASSERT(text < tool); TEST_ASSERT(command != NULL); TEST_ASSERT(description != NULL); TEST_ASSERT(description < command); buf_free(&b); tool_calls_free(&calls); request_free(&r); } static void test_parse_short_dsml_and_canonical_suffix(void) { const char *generated = "need a tool" "\n" "\n" "list files\n" "ls -la\n" "\n" ""; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, false, &content, &reasoning, &calls)); TEST_ASSERT(reasoning && !strcmp(reasoning, "need a tool")); TEST_ASSERT(content && content[0] == '\0'); TEST_ASSERT(calls.len == 1); request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; r.tool_orders = make_bash_order(); char *suffix = build_tool_checkpoint_suffix(&r, content, reasoning, &calls); const char *command = strstr(suffix, "name=\"command\""); const char *description = strstr(suffix, "name=\"description\""); TEST_ASSERT(command != NULL); TEST_ASSERT(description != NULL); TEST_ASSERT(description < command); TEST_ASSERT(strstr(suffix, "") != NULL); TEST_ASSERT(strstr(suffix, "<|end▁of▁sentence|>") != NULL); free(suffix); free(content); free(reasoning); tool_calls_free(&calls); request_free(&r); } static void test_parse_glm_tool_call_message(void) { const char *generated = "need bashOK\n\n" "bash" "commandecho hi" "timeout10" ""; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_GLM, generated, true, &content, &reasoning, &calls)); TEST_ASSERT(reasoning && !strcmp(reasoning, "need bash")); TEST_ASSERT(content && !strcmp(content, "OK")); TEST_ASSERT(calls.len == 1); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "bash")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"command\": \"echo hi\"") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"timeout\": \"10\"") != NULL); TEST_ASSERT(calls.raw_tool_text && !strncmp(calls.raw_tool_text, "\n\nbash", strlen("\n\nbash"))); free(content); free(reasoning); tool_calls_free(&calls); } static void test_dsml_parser_recovers_loose_nested_parameters(void) { const char *generated = "review done\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"path\">/private/tmp/tetris.c" DS4_PARAM_END "\n" DS4_PARAM_START " name=\"edits\">\n" DS4_PARAM_START " name=\"oldText\" string=\"true\">old <text>" DS4_PARAM_END "\n" DS4_PARAM_START " name=\"newText\" string=\"true\">new text" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, false, &content, &reasoning, &calls)); TEST_ASSERT(content && !strcmp(content, "review done")); TEST_ASSERT(calls.len == 1); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "edit")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"path\": \"/private/tmp/tetris.c\"") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"edits\": {") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"oldText\":\"old <text>\"") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"newText\":\"new text\"") != NULL); free(content); free(reasoning); tool_calls_free(&calls); } /* Repair only a missing wrapper around complete invocations. Never turn an * incomplete command, file body or argument list into an executable call. */ static void test_dsml_repair_produces_parseable_calls(void) { char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; buf repaired = {0}; /* === TEST 1: Full DSML - missing === */ { const char *broken = "thinking done\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">ls -la" DS4_PARAM_END "\n" DS4_INVOKE_END "\n"; /* Missing: DS4_TOOL_CALLS_END */ buf_free(&repaired); TEST_ASSERT(try_repair_dsml(broken, strlen(broken), &repaired)); TEST_ASSERT(parse_generated_message_ex(repaired.ptr, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "bash")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"command\": \"ls -la\"") != NULL); free(content); free(reasoning); tool_calls_free(&calls); } /* Missing : even a closed value may have further arguments. */ { const char *broken = "\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"path\" string=\"true\">/tmp/test.c" DS4_PARAM_END "\n"; /* Missing: DS4_INVOKE_END, DS4_TOOL_CALLS_END */ buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(broken, strlen(broken), &repaired)); TEST_ASSERT(repaired.len == 0); } /* A command prefix is not a complete command, even if it looks plausible. */ { const char *broken = "\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">echo hello"; /* Missing: DS4_PARAM_END, DS4_INVOKE_END, DS4_TOOL_CALLS_END */ buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(broken, strlen(broken), &repaired)); TEST_ASSERT(repaired.len == 0); } /* === TEST 4: Short DSML - missing closing tags === */ { const char *broken = "\n\n" DS4_TOOL_CALLS_START_SHORT "\n" DS4_INVOKE_START_SHORT " name=\"write_file\">\n" DS4_PARAM_START_SHORT " name=\"path\" string=\"true\">/tmp/out.txt" DS4_PARAM_END_SHORT "\n" DS4_PARAM_START_SHORT " name=\"content\" string=\"true\">hello world" DS4_PARAM_END_SHORT "\n" DS4_INVOKE_END_SHORT "\n"; /* Missing: DS4_TOOL_CALLS_END_SHORT */ buf_free(&repaired); TEST_ASSERT(try_repair_dsml(broken, strlen(broken), &repaired)); TEST_ASSERT(parse_generated_message_ex(repaired.ptr, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "write_file")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"path\": \"/tmp/out.txt\"") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "\"content\": \"hello world\"") != NULL); free(content); free(reasoning); tool_calls_free(&calls); } /* === TEST 5: Plain XML - missing closing tags === */ { const char *broken = "\n\n" "\n" "\n" "pwd\n" "\n"; /* Missing: */ buf_free(&repaired); TEST_ASSERT(try_repair_dsml(broken, strlen(broken), &repaired)); TEST_ASSERT(parse_generated_message_ex(repaired.ptr, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "execute_command")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"command\": \"pwd\"") != NULL); free(content); free(reasoning); tool_calls_free(&calls); } /* === TEST 6: Balanced text should NOT be modified === */ { const char *balanced = "\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">ls" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(balanced, strlen(balanced), &repaired)); /* No repair needed */ } /* === TEST 7: No DSML tags should return false === */ { const char *no_dsml = "just plain text, no tools"; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(no_dsml, strlen(no_dsml), &repaired)); } /* === TEST 8: Balanced DSML with no invoke is not repaired === */ { const char *balanced_no_invoke = "Let me analyze this.\n\n" DS4_TOOL_CALLS_START "The write tool truncates this too, at what looks like the same content location." DS4_TOOL_CALLS_END; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(balanced_no_invoke, strlen(balanced_no_invoke), &repaired)); } /* === TEST 9: Balanced short DSML with no invoke is not repaired === */ { const char *balanced_short_no_invoke = "thinking...\n\n" DS4_TOOL_CALLS_START_SHORT "some content here" DS4_TOOL_CALLS_END_SHORT; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(balanced_short_no_invoke, strlen(balanced_short_no_invoke), &repaired)); } /* === TEST 10: Balanced plain XML DSML with no invoke is not repaired === */ { const char *balanced_xml_no_invoke = "Let me think.\n\n" "" "I need to use a tool but I don't know which one." ""; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(balanced_xml_no_invoke, strlen(balanced_xml_no_invoke), &repaired)); } /* === TEST 11: DSML mentioned inside thinking is not repaired === */ { const char *thinking_quote = "The protocol uses " DS4_TOOL_CALLS_START "some explanatory text" DS4_TOOL_CALLS_END ", but this is only a quote.\nFinal answer."; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(thinking_quote, strlen(thinking_quote), &repaired)); } /* === TEST 12: Extra closing tags are unrecoverable, not truncation === */ { const char *orphan_close = "done\n\n" DS4_TOOL_CALLS_START DS4_TOOL_CALLS_END DS4_TOOL_CALLS_END; buf_free(&repaired); TEST_ASSERT(!try_repair_dsml(orphan_close, strlen(orphan_close), &repaired)); } /* === TEST 13: Real DSML after thinking still repairs normally === */ { const char *broken_after_think = "" DS4_TOOL_CALLS_START "quoted DSML, not executable" DS4_TOOL_CALLS_END "\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">date" DS4_PARAM_END "\n" DS4_INVOKE_END "\n"; buf_free(&repaired); TEST_ASSERT(try_repair_dsml(broken_after_think, strlen(broken_after_think), &repaired)); TEST_ASSERT(parse_generated_message_ex(repaired.ptr, true, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "bash")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"command\": \"date\"") != NULL); free(content); free(reasoning); tool_calls_free(&calls); } buf_free(&repaired); } static void test_tool_parse_failure_returns_recoverable_finish(void) { const char *generated = "trying a tool\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START ">\n" DS4_TOOL_CALLS_END; char err[128] = {0}; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; const char *finish = "tool_calls"; bool recovered = false; TEST_ASSERT(!parse_generated_message_for_response(generated, true, true, false, &finish, err, sizeof(err), &content, &reasoning, &calls, &recovered)); TEST_ASSERT(recovered); TEST_ASSERT(!strcmp(finish, "stop")); TEST_ASSERT(!strcmp(err, "invalid tool call")); TEST_ASSERT(content && strstr(content, DS4_TOOL_CALLS_START) != NULL); TEST_ASSERT(reasoning == NULL); TEST_ASSERT(calls.len == 0); free(content); free(reasoning); tool_calls_free(&calls); } static void test_invalid_dsml_tool_error_suffix_includes_system_prompt(void) { request r = {0}; r.think_mode = DS4_THINK_HIGH; r.prompt_text = xstrdup( "<|begin▁of▁sentence|>" "## Tools\nschema\n\nSystem rule\n\n" "<|User|>Hi<|Assistant|>"); thinking_state st = {.inside = true}; char *suffix = build_invalid_dsml_tool_error_suffix(&r, &st, "missing invoke name"); TEST_ASSERT(suffix != NULL); TEST_ASSERT(strstr(suffix, "<|end▁of▁sentence|><|User|>") == suffix); TEST_ASSERT(strstr(suffix, "Tool error: invalid DSML tool call: missing invoke name") != NULL); TEST_ASSERT(strstr(suffix, "The previous assistant output was not executed") != NULL); TEST_ASSERT(strstr(suffix, "System prompt reminder:\n## Tools\nschema\n\nSystem rule") != NULL); TEST_ASSERT(strstr(suffix, "<|User|>Hi") == NULL); TEST_ASSERT(strstr(suffix, "<|Assistant|>") != NULL); free(suffix); free(r.prompt_text); } static void test_invalid_glm_tool_error_suffix(void) { request r; request_init(&r, REQ_CHAT, 128); r.model_syntax = SERVER_MODEL_SYNTAX_GLM; r.think_mode = DS4_THINK_HIGH; thinking_state st = {.inside = true}; char *suffix = build_invalid_tool_call_error_suffix(&r, &st, "missing arg_value"); TEST_ASSERT(suffix != NULL); TEST_ASSERT(strstr(suffix, "<|observation|>") == suffix); TEST_ASSERT(strstr(suffix, "Tool error: invalid GLM tool call: missing arg_value") != NULL); TEST_ASSERT(strstr(suffix, "<|assistant|>") != NULL); TEST_ASSERT(strstr(suffix, "DSML") == NULL); TEST_ASSERT(strstr(suffix, "<|end▁of▁sentence|>") == NULL); free(suffix); request_free(&r); } static void test_tool_recovery_output_budget(void) { TEST_ASSERT(server_decode_budget(24, 0, 100) == 24); TEST_ASSERT(server_decode_budget(24, 17, 100) == 7); TEST_ASSERT(server_decode_budget(24, 17, 3) == 3); TEST_ASSERT(server_decode_budget(24, 24, 100) == 0); TEST_ASSERT(server_decode_budget(24, 25, 100) == 0); TEST_ASSERT(server_decode_budget(24, 0, 0) == 0); TEST_ASSERT(server_decode_budget(24, 0, -1) == 0); TEST_ASSERT(server_decode_budget(-1, 0, 100) == 0); TEST_ASSERT(server_decode_budget(INT_MAX, INT_MAX - 1, INT_MAX) == 1); /* Logging malformed, short tool output must not subtract from its length. */ TEST_ASSERT(find_any_tool_start("") != NULL); TEST_ASSERT(find_any_tool_start("" DS4_PARAM_START " name=\"content\" string=\"true\">unfinished", "writecontentunfinished", }; const char *reasons[] = {"length", "stop"}; for (int glm = 0; glm <= 1; glm++) { for (int i = 0; i < 2; i++) { const char *finish = reasons[i]; char *content = NULL, *reasoning = NULL; char err[128] = {0}; tool_calls calls = {0}; bool recovered = false; TEST_ASSERT(!parse_generated_message_for_response_for_syntax( glm ? SERVER_MODEL_SYNTAX_GLM : SERVER_MODEL_SYNTAX_DEEPSEEK, raw[glm], true, true, false, &finish, err, sizeof(err), &content, &reasoning, &calls, &recovered, NULL)); TEST_ASSERT(!strcmp(finish, reasons[i])); TEST_ASSERT(recovered && calls.len == 0); TEST_ASSERT(content && !strcmp(content, raw[glm])); free(content); free(reasoning); tool_calls_free(&calls); } } } static void test_invalid_qwen_tool_error_suffix(void) { request r; request_init(&r, REQ_CHAT, 128); r.model_syntax = SERVER_MODEL_SYNTAX_QWEN; r.think_mode = DS4_THINK_HIGH; thinking_state st = {.inside = true}; char *suffix = build_invalid_tool_call_error_suffix(&r, &st, "missing function name"); TEST_ASSERT(suffix != NULL); TEST_ASSERT(strstr(suffix, "\n\n\n<|im_end|>\n<|im_start|>user") == suffix); TEST_ASSERT(strstr(suffix, "Tool error: invalid Qwen tool call: missing function name") != NULL); TEST_ASSERT(strstr(suffix, "The previous assistant output was not executed") != NULL); TEST_ASSERT(strstr(suffix, "<|im_end|>\n<|im_start|>assistant\n\n") != NULL); TEST_ASSERT(strstr(suffix, "DSML") == NULL); TEST_ASSERT(strstr(suffix, "<|end▁of▁sentence|>") == NULL); free(suffix); request_free(&r); } static void test_thinking_dsml_is_not_executable_before_think_close(void) { const char *generated = "I might mention a malformed or tentative tool call here:\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">true" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END "\nBut it is still reasoning, not an assistant action.Final answer."; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, true, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 0); TEST_ASSERT(reasoning && strstr(reasoning, DS4_TOOL_CALLS_START) != NULL); TEST_ASSERT(content && !strcmp(content, "Final answer.")); free(content); free(reasoning); tool_calls_free(&calls); } static void test_thinking_dsml_after_think_close_is_executable(void) { const char *generated = "need a shell check\n\n" DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">\n" DS4_PARAM_START " name=\"command\" string=\"true\">pwd" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, true, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(reasoning && !strcmp(reasoning, "need a shell check")); TEST_ASSERT(content && content[0] == '\0'); TEST_ASSERT(calls.v[0].name && !strcmp(calls.v[0].name, "bash")); TEST_ASSERT(strstr(calls.v[0].arguments, "\"command\": \"pwd\"") != NULL); free(content); free(reasoning); tool_calls_free(&calls); } static void test_tool_checkpoint_suffix_is_future_prompt_canonical(void) { tool_schema_orders orders = make_bash_order(); const char *tool_schemas = "{\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"command\":{},\"description\":{},\"timeout\":{}}}}"; chat_msgs prefix_msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("inspect"); chat_msgs_push(&prefix_msgs, user); char *prompt_text = render_chat_prompt_text(&prefix_msgs, tool_schemas, &orders, DS4_THINK_HIGH); const char *generated = "need a tool\n\n" DS4_TOOL_CALLS_START "\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">cd /tmp && git diff 2>/dev/null\n" "<|DSML|parameter name=\"timeout\" string=\"false\">10\n" "\n" ""; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(strstr(calls.v[0].arguments, "cd /tmp && git diff 2>/dev/null") != NULL); TEST_ASSERT(strstr(calls.v[0].arguments, "&&") == NULL); request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; r.tool_orders = orders; memset(&orders, 0, sizeof(orders)); char *suffix = build_tool_checkpoint_suffix(&r, content, reasoning, &calls); TEST_ASSERT(strstr(suffix, "cd /tmp && git diff 2>/dev/null") != NULL); TEST_ASSERT(strstr(suffix, "&&") == NULL); TEST_ASSERT(strstr(suffix, "2>/dev/null") == NULL); buf canonical = {0}; buf_puts(&canonical, prompt_text); buf_puts(&canonical, suffix); chat_msgs history_msgs = {0}; chat_msg user2 = {0}; user2.role = xstrdup("user"); user2.content = xstrdup("inspect"); chat_msgs_push(&history_msgs, user2); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup(reasoning ? reasoning : ""); assistant.content = xstrdup(content ? content : ""); assistant.calls = calls; memset(&calls, 0, sizeof(calls)); chat_msgs_push(&history_msgs, assistant); char *future_prompt = render_chat_prompt_text(&history_msgs, tool_schemas, &r.tool_orders, DS4_THINK_HIGH); TEST_ASSERT(!strcmp(canonical.ptr, future_prompt)); free(future_prompt); buf_free(&canonical); free(suffix); free(prompt_text); free(content); free(reasoning); chat_msgs_free(&history_msgs); chat_msgs_free(&prefix_msgs); tool_calls_free(&calls); request_free(&r); tool_schema_orders_free(&orders); } static void test_glm_tool_checkpoint_suffix_is_canonical(void) { tool_schema_orders orders = make_bash_order(); const char *tool_schemas = "{\"name\":\"bash\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"command\":{},\"description\":{}}}}"; chat_msgs prefix_msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("inspect"); chat_msgs_push(&prefix_msgs, user); char *prompt_text = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &prefix_msgs, tool_schemas, &orders, DS4_THINK_HIGH); const char *generated = "need bashdone\n\n" "bash" "descriptionlist files" "commandls -la" ""; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex_for_syntax( SERVER_MODEL_SYNTAX_GLM, generated, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); request r; request_init(&r, REQ_CHAT, 128); r.model_syntax = SERVER_MODEL_SYNTAX_GLM; r.think_mode = DS4_THINK_HIGH; r.tool_orders = orders; memset(&orders, 0, sizeof(orders)); char *suffix = build_tool_checkpoint_suffix(&r, content, reasoning, &calls); TEST_ASSERT(strstr(suffix, "need bashdone\n\nbash") != NULL); TEST_ASSERT(strstr(suffix, "<|end▁of▁sentence|>") == NULL); TEST_ASSERT(strstr(suffix, "DSML") == NULL); buf canonical = {0}; buf_puts(&canonical, prompt_text); buf_puts(&canonical, suffix); chat_msgs history_msgs = {0}; chat_msg user2 = {0}; user2.role = xstrdup("user"); user2.content = xstrdup("inspect"); chat_msgs_push(&history_msgs, user2); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup(reasoning ? reasoning : ""); assistant.content = xstrdup(content ? content : ""); assistant.calls = calls; memset(&calls, 0, sizeof(calls)); chat_msgs_push(&history_msgs, assistant); char *future_prompt = render_chat_prompt_text_for_syntax( SERVER_MODEL_SYNTAX_GLM, &history_msgs, tool_schemas, &r.tool_orders, DS4_THINK_HIGH); TEST_ASSERT(!strcmp(canonical.ptr, future_prompt)); free(future_prompt); buf_free(&canonical); free(suffix); free(prompt_text); free(content); free(reasoning); chat_msgs_free(&history_msgs); chat_msgs_free(&prefix_msgs); tool_calls_free(&calls); request_free(&r); tool_schema_orders_free(&orders); } static void test_tool_checkpoint_minifies_json_parameters(void) { tool_schema_orders orders = {0}; tool_schema_orders_add_json(&orders, "{\"name\":\"edit\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"path\":{},\"edits\":{}}}}"); const char *tool_schemas = "{\"name\":\"edit\",\"parameters\":{\"type\":\"object\",\"properties\":{" "\"path\":{},\"edits\":{}}}}"; chat_msgs prefix_msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("edit"); chat_msgs_push(&prefix_msgs, user); char *prompt_text = render_chat_prompt_text(&prefix_msgs, tool_schemas, &orders, DS4_THINK_HIGH); const char *generated = "need edit\n\n" DS4_TOOL_CALLS_START "\n" "<|DSML|invoke name=\"edit\">\n" "<|DSML|parameter name=\"path\" string=\"true\">/tmp/file\n" "<|DSML|parameter name=\"edits\" string=\"false\">" "[{\"oldText\": \"status=created\", \"newText\": \"status=created\\nstatus2=resumed\"}]" "\n" "\n" ""; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, false, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; r.tool_orders = orders; memset(&orders, 0, sizeof(orders)); char *suffix = build_tool_checkpoint_suffix(&r, content, reasoning, &calls); buf canonical = {0}; buf_puts(&canonical, prompt_text); buf_puts(&canonical, suffix); chat_msgs history_msgs = {0}; chat_msg user2 = {0}; user2.role = xstrdup("user"); user2.content = xstrdup("edit"); chat_msgs_push(&history_msgs, user2); chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup(reasoning ? reasoning : ""); assistant.content = xstrdup(content ? content : ""); assistant.calls = calls; memset(&calls, 0, sizeof(calls)); chat_msgs_push(&history_msgs, assistant); char *future_prompt = render_chat_prompt_text(&history_msgs, tool_schemas, &r.tool_orders, DS4_THINK_HIGH); TEST_ASSERT(!strcmp(canonical.ptr, future_prompt)); free(future_prompt); buf_free(&canonical); free(suffix); free(prompt_text); free(content); free(reasoning); chat_msgs_free(&history_msgs); chat_msgs_free(&prefix_msgs); tool_calls_free(&calls); request_free(&r); tool_schema_orders_free(&orders); } static void test_tool_memory_replays_sampled_dsml(void) { const char *generated = "need shell\n\n" DS4_TOOL_CALLS_START "\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">ls -la\n" "<|DSML|parameter name=\"timeout\" string=\"false\">10\n" "<|DSML|parameter name=\"description\" string=\"true\">list files\n" "\n" ""; char *content = NULL; char *reasoning = NULL; tool_calls sampled = {0}; TEST_ASSERT(parse_generated_message_ex(generated, false, &content, &reasoning, &sampled)); TEST_ASSERT(sampled.len == 1); server s; memset(&s, 0, sizeof(s)); pthread_mutex_init(&s.tool_mu, NULL); assign_tool_call_ids(&s, &sampled, API_OPENAI); TEST_ASSERT(sampled.v[0].id != NULL); TEST_ASSERT(!strncmp(sampled.v[0].id, "call_", 5)); tool_memory_remember(&s, &sampled); chat_msgs msgs = {0}; chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); assistant.reasoning = xstrdup(reasoning ? reasoning : ""); assistant.content = xstrdup(content ? content : ""); tool_call tc = {0}; tc.id = xstrdup(sampled.v[0].id); tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"description\":\"list files\",\"command\":\"ls -la\",\"timeout\":10}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); tool_replay_stats stats = {0}; tool_memory_attach_to_messages(&s, &msgs, &stats); TEST_ASSERT(msgs.v[0].calls.raw_tool_text != NULL); TEST_ASSERT(stats.mem == 1); TEST_ASSERT(stats.disk == 0); TEST_ASSERT(stats.canonical == 0); TEST_ASSERT(stats.missing_ids == 0); char *prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); const char *command = strstr(prompt, "name=\"command\""); const char *timeout = strstr(prompt, "name=\"timeout\""); const char *description = strstr(prompt, "name=\"description\""); TEST_ASSERT(command != NULL); TEST_ASSERT(timeout != NULL); TEST_ASSERT(description != NULL); TEST_ASSERT(command < timeout); TEST_ASSERT(timeout < description); free(prompt); chat_msgs_free(&msgs); free(content); free(reasoning); tool_calls_free(&sampled); tool_memory_free(&s.tool_mem); pthread_mutex_destroy(&s.tool_mu); } static void test_anthropic_tool_memory_replays_sampled_dsml(void) { const char *sampled_dsml = "\n\n" DS4_TOOL_CALLS_START "\n" "<|DSML|invoke name=\"Bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">ls -la\n" "<|DSML|parameter name=\"description\" string=\"true\">list files\n" "\n" DS4_TOOL_CALLS_END; server s; memset(&s, 0, sizeof(s)); pthread_mutex_init(&s.tool_mu, NULL); tool_memory_put(&s, "toolu_exact", sampled_dsml); const char *json = "[" "{\"role\":\"assistant\",\"content\":[" "{\"type\":\"tool_use\",\"id\":\"toolu_exact\",\"name\":\"Bash\"," "\"input\":{\"description\":\"list files\",\"command\":\"ls -la\"}}" "]}," "{\"role\":\"user\",\"content\":[" "{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_exact\",\"content\":\"ok\"}" "]}" "]"; const char *p = json; chat_msgs msgs = {0}; TEST_ASSERT(parse_anthropic_messages(&p, &msgs)); TEST_ASSERT(msgs.len == 2); TEST_ASSERT(msgs.v[1].tool_call_id && !strcmp(msgs.v[1].tool_call_id, "toolu_exact")); stop_list ids = {0}; collect_tool_call_ids(&msgs, &ids); TEST_ASSERT(id_list_contains(&ids, "toolu_exact")); id_list_free(&ids); tool_replay_stats stats = {0}; tool_memory_attach_to_messages(&s, &msgs, &stats); TEST_ASSERT(msgs.v[0].calls.raw_tool_text != NULL); TEST_ASSERT(stats.mem == 1); TEST_ASSERT(stats.canonical == 0); char *prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); const char *command = strstr(prompt, "name=\"command\""); const char *description = strstr(prompt, "name=\"description\""); TEST_ASSERT(command != NULL); TEST_ASSERT(description != NULL); TEST_ASSERT(command < description); free(prompt); chat_msgs_free(&msgs); tool_memory_free(&s.tool_mem); pthread_mutex_destroy(&s.tool_mu); } static void test_anthropic_live_tail_renders_tool_results_only(void) { request r; request_init(&r, REQ_CHAT, 128); r.api = API_ANTHROPIC; r.think_mode = DS4_THINK_HIGH; chat_msgs msgs = {0}; chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); tool_call tc = {0}; tc.id = xstrdup("toolu_live"); tc.name = xstrdup("Bash"); tc.arguments = xstrdup("{\"command\":\"pwd\"}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("/tmp"); chat_msg_add_tool_call_id(&user, "toolu_live"); chat_msgs_push(&msgs, user); /* Anthropic system text is parsed separately and appended to chat_msgs for * rendering. The live-tail finder must ignore it when locating the final * tool_result run. */ chat_msg system = {0}; system.role = xstrdup("system"); system.content = xstrdup("You are terse."); chat_msgs_push(&msgs, system); anthropic_prepare_live_continuation(NULL, &r, &msgs); TEST_ASSERT(r.anthropic_live_call_ids.len == 1); TEST_ASSERT(!strcmp(r.anthropic_live_call_ids.v[0], "toolu_live")); TEST_ASSERT(r.anthropic_live_suffix_text != NULL); TEST_ASSERT(!strncmp(r.anthropic_live_suffix_text, "<|end▁of▁sentence|><|User|>", strlen("<|end▁of▁sentence|><|User|>"))); TEST_ASSERT(strstr(r.anthropic_live_suffix_text, "/tmp") != NULL); TEST_ASSERT(strstr(r.anthropic_live_suffix_text, "<|Assistant|>") != NULL); TEST_ASSERT(strstr(r.anthropic_live_suffix_text, "Bash") == NULL); chat_msgs_free(&msgs); request_free(&r); } static void test_anthropic_tool_result_id_validation(void) { server s = {0}; server_slot slot; test_server_bind_slot(&s, &slot); pthread_mutex_init(&s.tool_mu, NULL); chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("out"); chat_msg_add_tool_call_id(&user, "toolu_missing"); chat_msgs_push(&msgs, user); char err[160] = {0}; TEST_ASSERT(!anthropic_validate_tool_results(&s, &msgs, NULL, err, sizeof(err))); TEST_ASSERT(strstr(err, "Anthropic continuation state is not available") != NULL); pthread_mutex_lock(&s.tool_mu); slot.anthropic_live.valid = true; slot.anthropic_live.live_tokens = 10; id_list_push_unique(&slot.anthropic_live.call_ids, "toolu_missing"); pthread_mutex_unlock(&s.tool_mu); bool needs_live_tool_state = false; err[0] = '\0'; TEST_ASSERT(anthropic_validate_tool_results(&s, &msgs, &needs_live_tool_state, err, sizeof(err))); TEST_ASSERT(needs_live_tool_state); chat_msgs_free(&msgs); live_tool_state_free(&slot.anthropic_live); pthread_mutex_destroy(&s.tool_mu); } static void test_anthropic_full_replay_allows_unknown_live_id(void) { server s = {0}; pthread_mutex_init(&s.tool_mu, NULL); chat_msgs msgs = {0}; chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); tool_call tc = {0}; tc.id = xstrdup("toolu_replay"); tc.name = xstrdup("Bash"); tc.arguments = xstrdup("{\"command\":\"pwd\"}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("/tmp"); chat_msg_add_tool_call_id(&user, "toolu_replay"); chat_msgs_push(&msgs, user); bool needs_live_tool_state = false; char err[160] = {0}; TEST_ASSERT(anthropic_validate_tool_results(&s, &msgs, &needs_live_tool_state, err, sizeof(err))); TEST_ASSERT(!needs_live_tool_state); chat_msgs_free(&msgs); pthread_mutex_destroy(&s.tool_mu); } static void test_anthropic_tool_use_parses_before_role(void) { server s = {0}; server_slot slot; test_server_bind_slot(&s, &slot); pthread_mutex_init(&s.tool_mu, NULL); /* GitHub #127 regression: Crush can replay full Anthropic history with * message objects serialized as {"content": ..., "role": ...}. The parser * must still remember prior assistant tool_use ids, otherwise old * tool_result blocks are mistaken for live-only continuations and rejected * once the live frontier has moved on to newer tool calls. */ pthread_mutex_lock(&s.tool_mu); slot.anthropic_live.valid = true; slot.anthropic_live.live_tokens = 100; id_list_push_unique(&slot.anthropic_live.call_ids, "toolu_current"); pthread_mutex_unlock(&s.tool_mu); const char *json = "[" "{\"content\":[" "{\"type\":\"tool_use\",\"id\":\"toolu_old\",\"name\":\"Bash\"," "\"input\":{\"command\":\"ls\"}}" "],\"role\":\"assistant\"}," "{\"role\":\"user\",\"content\":[" "{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_old\",\"content\":\"ok\"}" "]}," "{\"role\":\"user\",\"content\":\"continue\"}" "]"; const char *p = json; chat_msgs msgs = {0}; TEST_ASSERT(parse_anthropic_messages(&p, &msgs)); TEST_ASSERT(msgs.len == 3); TEST_ASSERT(msgs.v[0].calls.len == 1); TEST_ASSERT(msgs.v[0].calls.v[0].id && !strcmp(msgs.v[0].calls.v[0].id, "toolu_old")); bool needs_live_tool_state = false; char err[160] = {0}; TEST_ASSERT(anthropic_validate_tool_results(&s, &msgs, &needs_live_tool_state, err, sizeof(err))); TEST_ASSERT(!needs_live_tool_state); chat_msgs_free(&msgs); live_tool_state_free(&slot.anthropic_live); pthread_mutex_destroy(&s.tool_mu); } static void test_tool_checkpoint_canonicalization_gate_exact_replay(void) { server s; memset(&s, 0, sizeof(s)); tool_calls calls = {0}; tool_call tc = {0}; tc.id = xstrdup("call_exact"); tc.name = xstrdup("bash"); tc.arguments = xstrdup("{}"); tool_calls_push(&calls, tc); calls.raw_tool_text = xstrdup( "\n\n" DS4_TOOL_CALLS_START "\n" "<|DSML|invoke name=\"bash\">\n" "\n" DS4_TOOL_CALLS_END); TEST_ASSERT(!should_canonicalize_tool_checkpoint(&s, &calls)); s.disable_exact_dsml_tool_replay = true; TEST_ASSERT(should_canonicalize_tool_checkpoint(&s, &calls)); s.disable_exact_dsml_tool_replay = false; free(calls.raw_tool_text); calls.raw_tool_text = NULL; TEST_ASSERT(should_canonicalize_tool_checkpoint(&s, &calls)); tool_calls_free(&calls); } static void test_responses_live_tail_renders_tool_outputs_only(void) { request r; request_init(&r, REQ_CHAT, 128); r.api = API_RESPONSES; r.think_mode = DS4_THINK_HIGH; chat_msgs msgs = {0}; chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); tool_call tc = {0}; tc.id = xstrdup("call_live"); tc.name = xstrdup("exec_command"); tc.arguments = xstrdup("{\"cmd\":\"pwd\"}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.tool_call_id = xstrdup("call_live"); tool.content = xstrdup("/tmp"); chat_msgs_push(&msgs, tool); responses_prepare_live_continuation(NULL, &r, &msgs); TEST_ASSERT(r.responses_live_call_ids.len == 1); TEST_ASSERT(!strcmp(r.responses_live_call_ids.v[0], "call_live")); TEST_ASSERT(r.responses_live_suffix_text != NULL); TEST_ASSERT(!strncmp(r.responses_live_suffix_text, "<|end▁of▁sentence|><|User|>", strlen("<|end▁of▁sentence|><|User|>"))); TEST_ASSERT(strstr(r.responses_live_suffix_text, "/tmp") != NULL); TEST_ASSERT(strstr(r.responses_live_suffix_text, "<|Assistant|>") != NULL); TEST_ASSERT(strstr(r.responses_live_suffix_text, "exec_command") == NULL); chat_msgs_free(&msgs); request_free(&r); } static void test_responses_tool_output_id_validation(void) { server s = {0}; server_slot slot; test_server_bind_slot(&s, &slot); pthread_mutex_init(&s.tool_mu, NULL); chat_msgs msgs = {0}; chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.tool_call_id = xstrdup("call_missing"); tool.content = xstrdup("out"); chat_msgs_push(&msgs, tool); char err[160] = {0}; TEST_ASSERT(!responses_validate_tool_outputs(&s, &msgs, DS4_THINK_HIGH, NULL, NULL, err, sizeof(err))); TEST_ASSERT(strstr(err, "Responses continuation state is not available") != NULL); pthread_mutex_lock(&s.tool_mu); slot.responses_live.valid = true; slot.responses_live.live_tokens = 10; id_list_push_unique(&slot.responses_live.call_ids, "call_missing"); pthread_mutex_unlock(&s.tool_mu); err[0] = '\0'; bool needs_live_tool_state = false; TEST_ASSERT(responses_validate_tool_outputs(&s, &msgs, DS4_THINK_HIGH, &needs_live_tool_state, NULL, err, sizeof(err))); TEST_ASSERT(needs_live_tool_state); chat_msgs_free(&msgs); live_tool_state_free(&slot.responses_live); pthread_mutex_destroy(&s.tool_mu); } static void test_responses_stateless_tool_replay_requires_reasoning(void) { server s = {0}; server_slot slot; test_server_bind_slot(&s, &slot); pthread_mutex_init(&s.tool_mu, NULL); chat_msgs msgs = {0}; chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); tool_call tc = {0}; tc.id = xstrdup("call_replay"); tc.name = xstrdup("exec_command"); tc.arguments = xstrdup("{\"cmd\":\"pwd\"}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.tool_call_id = xstrdup("call_replay"); tool.content = xstrdup("/tmp"); chat_msgs_push(&msgs, tool); char err[160] = {0}; bool needs_live_reasoning = false; bool needs_live_tool_state = false; TEST_ASSERT(responses_validate_tool_outputs(&s, &msgs, DS4_THINK_HIGH, &needs_live_tool_state, &needs_live_reasoning, err, sizeof(err))); TEST_ASSERT(!needs_live_tool_state); TEST_ASSERT(needs_live_reasoning); pthread_mutex_lock(&s.tool_mu); slot.responses_live.valid = true; slot.responses_live.live_tokens = 123; id_list_push_unique(&slot.responses_live.call_ids, "call_replay"); pthread_mutex_unlock(&s.tool_mu); err[0] = '\0'; needs_live_reasoning = false; needs_live_tool_state = false; TEST_ASSERT(responses_validate_tool_outputs(&s, &msgs, DS4_THINK_HIGH, &needs_live_tool_state, &needs_live_reasoning, err, sizeof(err))); TEST_ASSERT(!needs_live_tool_state); TEST_ASSERT(needs_live_reasoning); free(msgs.v[0].reasoning); msgs.v[0].reasoning = xstrdup("replayed hidden reasoning"); err[0] = '\0'; needs_live_reasoning = false; needs_live_tool_state = false; TEST_ASSERT(responses_validate_tool_outputs(&s, &msgs, DS4_THINK_HIGH, &needs_live_tool_state, &needs_live_reasoning, err, sizeof(err))); TEST_ASSERT(!needs_live_tool_state); TEST_ASSERT(!needs_live_reasoning); free(msgs.v[0].reasoning); msgs.v[0].reasoning = NULL; err[0] = '\0'; needs_live_reasoning = false; needs_live_tool_state = false; TEST_ASSERT(responses_validate_tool_outputs(&s, &msgs, DS4_THINK_NONE, &needs_live_tool_state, &needs_live_reasoning, err, sizeof(err))); TEST_ASSERT(!needs_live_tool_state); TEST_ASSERT(!needs_live_reasoning); chat_msgs_free(&msgs); live_tool_state_free(&slot.responses_live); pthread_mutex_destroy(&s.tool_mu); } static void test_responses_visible_suffix_matches_client_replay(void) { request r; request_init(&r, REQ_CHAT, 128); r.api = API_RESPONSES; r.think_mode = DS4_THINK_HIGH; r.reasoning_summary_emit = true; char *suffix = build_responses_visible_assistant_suffix(&r, "5", "hidden summary", NULL); TEST_ASSERT(strstr(suffix, "hidden summary") == NULL); TEST_ASSERT(strstr(suffix, "5") != NULL); free(suffix); tool_calls calls = {0}; tool_call tc = {0}; tc.id = xstrdup("call_live"); tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"pwd\"}"); tool_calls_push(&calls, tc); suffix = build_responses_visible_assistant_suffix(&r, "", "tool summary", &calls); TEST_ASSERT(strstr(suffix, "tool summary") != NULL); TEST_ASSERT(strstr(suffix, "<|DSML|tool_calls>") != NULL); free(suffix); tool_calls_free(&calls); request_free(&r); } static void test_exact_dsml_tool_replay_can_be_disabled(void) { const char *dsml = "\n\n<|DSML|tool_calls>\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">pwd\n" "\n" ""; server s = {0}; pthread_mutex_init(&s.tool_mu, NULL); tool_memory_put(&s, "call_disabled", dsml); s.disable_exact_dsml_tool_replay = true; chat_msgs msgs = {0}; chat_msg assistant = {0}; assistant.role = xstrdup("assistant"); tool_call tc = {0}; tc.id = xstrdup("call_disabled"); tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"canonical\"}"); tool_calls_push(&assistant.calls, tc); chat_msgs_push(&msgs, assistant); tool_replay_stats stats = {0}; tool_memory_attach_to_messages(&s, &msgs, &stats); TEST_ASSERT(msgs.v[0].calls.raw_tool_text == NULL); TEST_ASSERT(stats.canonical == 1); TEST_ASSERT(stats.missing_ids == 1); FILE *fp = tmpfile(); TEST_ASSERT(fp != NULL); uint64_t bytes = 123; TEST_ASSERT(kv_tool_map_write(&s, fp, dsml, &bytes)); TEST_ASSERT(bytes == 0); if (fp) fclose(fp); chat_msgs_free(&msgs); tool_memory_free(&s.tool_mem); pthread_mutex_destroy(&s.tool_mu); } static void test_dsml_decode_state_separates_structure_and_payload(void) { dsml_decode_tracker tracker; dsml_decode_tracker_init(&tracker); const char *prefix = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n"; TEST_ASSERT(dsml_decode_state_for_text(prefix, strlen(prefix)) == DSML_DECODE_STRUCTURAL); dsml_decode_tracker_update(&tracker, prefix, strlen(prefix)); TEST_ASSERT(tracker.decode == DSML_DECODE_STRUCTURAL); const char *path_param = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"path\" string=\"true\">/tmp/a.py"; TEST_ASSERT(dsml_decode_state_for_text(path_param, strlen(path_param)) == DSML_DECODE_STRING_BODY); dsml_decode_tracker_update(&tracker, path_param, strlen(path_param)); TEST_ASSERT(tracker.decode == DSML_DECODE_STRING_BODY); const char *path_closing = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"path\" string=\"true\">/tmp/a.py\n" DS4_PARAM_START " name=\"edits\" string=\"false\">[{"; TEST_ASSERT(dsml_decode_state_for_text(json_struct, strlen(json_struct)) == DSML_DECODE_JSON_STRUCTURAL); dsml_decode_tracker_init(&tracker); dsml_decode_tracker_update(&tracker, json_struct, strlen(json_struct)); TEST_ASSERT(tracker.decode == DSML_DECODE_JSON_STRUCTURAL); const char *json_string = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"edits\" string=\"false\">[{\"newText\":\"for i in"; TEST_ASSERT(dsml_decode_state_for_text(json_string, strlen(json_string)) == DSML_DECODE_JSON_STRING); dsml_decode_tracker_update(&tracker, json_string, strlen(json_string)); TEST_ASSERT(tracker.decode == DSML_DECODE_JSON_STRING); const char *done = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"edit\">\n" DS4_PARAM_START " name=\"edits\" string=\"false\">[]" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; TEST_ASSERT(dsml_decode_state_for_text(done, strlen(done)) == DSML_DECODE_OUTSIDE); dsml_decode_tracker_init(&tracker); dsml_decode_tracker_update(&tracker, done, strlen(done)); TEST_ASSERT(tracker.decode == DSML_DECODE_OUTSIDE); } static void test_tool_memory_max_ids_prunes_oldest(void) { const char *a_dsml = "\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"bash\">\n<|DSML|parameter name=\"command\" string=\"true\">a\n\n"; const char *b_dsml = "\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"bash\">\n<|DSML|parameter name=\"command\" string=\"true\">b\n\n"; const char *c_dsml = "\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"bash\">\n<|DSML|parameter name=\"command\" string=\"true\">c\n\n"; server s = {0}; pthread_mutex_init(&s.tool_mu, NULL); s.tool_mem.max_entries = 2; tool_memory_put(&s, "call_a", a_dsml); tool_memory_put(&s, "call_b", b_dsml); tool_memory_put(&s, "call_c", c_dsml); chat_msgs msgs = {0}; chat_msg a = {0}; a.role = xstrdup("assistant"); tool_call tc = {.id = xstrdup("call_a"), .name = xstrdup("bash"), .arguments = xstrdup("{}")}; tool_calls_push(&a.calls, tc); chat_msgs_push(&msgs, a); tool_replay_stats stats = {0}; tool_memory_attach_to_messages(&s, &msgs, &stats); TEST_ASSERT(msgs.v[0].calls.raw_tool_text == NULL); TEST_ASSERT(stats.canonical == 1); TEST_ASSERT(stats.missing_ids == 1); chat_msgs_free(&msgs); tool_memory_free(&s.tool_mem); pthread_mutex_destroy(&s.tool_mu); } static void test_decode_tracker_split_parameter_close(void) { for (size_t style = 0; style < sizeof(dsml_syntaxes) / sizeof(dsml_syntaxes[0]); style++) { const dsml_syntax *syn = &dsml_syntaxes[style]; for (int string_value = 0; string_value <= 1; string_value++) { buf raw = {0}; buf_printf(&raw, "%s%s name=\"bash\">%s name=\"command\" string=\"%s\">%s%s%s%s", syn->tool_calls_start, syn->invoke_start, syn->param_start, string_value ? "true" : "false", string_value ? "echo " : "{\"x\":\"a\\\"b\"}", syn->param_end, syn->invoke_end, syn->tool_calls_end); dsml_decode_tracker tracker; dsml_decode_tracker_init(&tracker); for (size_t n = 1; n <= raw.len; n++) { dsml_decode_tracker_update(&tracker, raw.ptr, n); dsml_decode_state expected = dsml_decode_state_for_text(raw.ptr, n); if (tracker.decode != expected) { fprintf(stderr, "split tool marker: style=%zu string=%d byte=%zu actual=%d expected=%d\n", style, string_value, n, tracker.decode, expected); TEST_ASSERT(tracker.decode == expected); break; } } TEST_ASSERT(tracker.decode == DSML_DECODE_OUTSIDE); buf_free(&raw); } } } static void test_glm_decode_tracker_boundaries(void) { const char *parts[] = { "prose bashcommand", "printf ''", "<", "/arg_", "value>", "descriptiontest", "", }; const dsml_decode_state expected[] = { DSML_DECODE_STRUCTURAL, DSML_DECODE_STRING_BODY, DSML_DECODE_STRING_BODY, DSML_DECODE_STRUCTURAL, DSML_DECODE_STRUCTURAL, DSML_DECODE_STRING_BODY, DSML_DECODE_OUTSIDE, }; /* Check both token-sized fragments and every byte boundary. */ for (int bytewise = 0; bytewise <= 1; bytewise++) { dsml_decode_tracker tracker; dsml_decode_tracker_init(&tracker); tracker.model_syntax = SERVER_MODEL_SYNTAX_GLM; buf raw = {0}; for (size_t i = 0; i < sizeof(parts) / sizeof(parts[0]); i++) { if (bytewise) { for (const char *p = parts[i]; *p; p++) { buf_putc(&raw, *p); dsml_decode_tracker_update(&tracker, raw.ptr, raw.len); } } else { buf_puts(&raw, parts[i]); dsml_decode_tracker_update(&tracker, raw.ptr, raw.len); } TEST_ASSERT(tracker.decode == expected[i]); } buf_free(&raw); } } static void test_tool_control_text_inside_arguments(void) { const char *body = "literal " DS4_TOOL_CALLS_END " "; for (int glm = 0; glm <= 1; glm++) { buf raw = {0}; buf_puts(&raw, "reason"); if (glm) { buf_puts(&raw, "writecontent"); } else { buf_puts(&raw, DS4_TOOL_CALLS_START DS4_INVOKE_START " name=\"write\">" DS4_PARAM_START " name=\"content\" string=\"true\">"); } buf_puts(&raw, body); dsml_decode_tracker tracker; dsml_decode_tracker_init(&tracker); tracker.model_syntax = glm ? SERVER_MODEL_SYNTAX_GLM : SERVER_MODEL_SYNTAX_DEEPSEEK; bool start = false, end = false, orphan = false; for (size_t n = 1; n <= raw.len; n++) { dsml_decode_tracker_update(&tracker, raw.ptr, n); } observe_tool_markers(&tracker, raw.ptr, &start, &end, &orphan); TEST_ASSERT(start && !end && !orphan); size_t scan = 0; TEST_ASSERT(!complete_tool_call_inside_thinking(tracker.model_syntax, raw.ptr, raw.len, &scan)); buf_puts(&raw, glm ? "" : DS4_PARAM_END DS4_INVOKE_END DS4_TOOL_CALLS_END); dsml_decode_tracker_update(&tracker, raw.ptr, raw.len); observe_tool_markers(&tracker, raw.ptr, &start, &end, &orphan); TEST_ASSERT(start && end && !orphan); tool_calls calls = {0}; char *content = NULL, *reasoning = NULL; TEST_ASSERT(parse_generated_message_ex_for_syntax(tracker.model_syntax, raw.ptr, true, &content, &reasoning, &calls)); TEST_ASSERT(calls.len == 1); TEST_ASSERT(reasoning && !strcmp(reasoning, "reason")); if (calls.len) { json_args args = {0}; TEST_ASSERT(json_args_parse(calls.v[0].arguments, &args)); TEST_ASSERT(args.len == 1 && !strcmp(args.v[0].value, body)); json_args_free(&args); } free(content); free(reasoning); tool_calls_free(&calls); buf_free(&raw); } } static void test_qwen_decode_tracker_markers(void) { const char *raw = "\n\n\n" "literal \n" "\n\n"; dsml_decode_tracker tracker; dsml_decode_tracker_init(&tracker); tracker.model_syntax = SERVER_MODEL_SYNTAX_QWEN; bool start = false, end = false, orphan = false; size_t len = strlen(raw); for (size_t n = 1; n <= len; n++) { dsml_decode_tracker_update(&tracker, raw, n); char *prefix = xstrndup(raw, n); observe_tool_markers(&tracker, prefix, &start, &end, &orphan); free(prefix); TEST_ASSERT(!end || n == len); TEST_ASSERT(!orphan); } TEST_ASSERT(start && end); TEST_ASSERT(tracker.mode == DSML_TRACK_DONE); } static void test_tool_body_escape_round_trip(void) { const char *ends[] = {DS4_PARAM_END, "", "", "", ""}; for (size_t i = 0; i < sizeof(ends) / sizeof(ends[0]); i++) { buf input = {0}, encoded = {0}, streamed = {0}; buf_printf(&input, "& < > " ' %s <%s &lt;%s &amp;lt;%s end", ends[i], ends[i] + 1, ends[i] + 1, ends[i] + 1); append_glm_tag_body_text(&encoded, input.ptr, ends[i]); char *decoded = xstrdup(encoded.ptr); ds4_tool_text_unescape(decoded, ends[i]); TEST_ASSERT(!strcmp(input.ptr, decoded)); free(decoded); size_t sent = 0; for (size_t n = 1; n <= encoded.len; n++) { size_t safe = tool_param_value_stream_safe_len(encoded.ptr, sent, n, ends[i], true); TEST_ASSERT(safe >= sent && safe <= n); char *part = xstrndup(encoded.ptr + sent, safe - sent); ds4_tool_text_unescape(part, ends[i]); buf_puts(&streamed, part); free(part); sent = safe; } TEST_ASSERT(sent == encoded.len); TEST_ASSERT(!strcmp(input.ptr, streamed.ptr)); buf_free(&input); buf_free(&encoded); buf_free(&streamed); } for (int glm = 0; glm <= 1; glm++) { tool_calls original = {0}, parsed = {0}; tool_call tc = {.name = xstrdup("write"), .arguments = xstrdup("{\"content\":\"

& < " '

\"}")}; tool_calls_push(&original, tc); buf raw = {0}; server_model_syntax syntax = glm ? SERVER_MODEL_SYNTAX_GLM : SERVER_MODEL_SYNTAX_DEEPSEEK; append_tool_calls_text_for_syntax(&raw, syntax, &original, NULL); char *content = NULL, *reasoning = NULL; TEST_ASSERT(parse_generated_message_ex_for_syntax(syntax, raw.ptr, false, &content, &reasoning, &parsed)); TEST_ASSERT(parsed.len == 1); if (parsed.len) { json_args args = {0}; TEST_ASSERT(json_args_parse(parsed.v[0].arguments, &args)); TEST_ASSERT(args.len == 1 && !strcmp(args.v[0].value, "

& < " '

")); json_args_free(&args); } free(content); free(reasoning); tool_calls_free(&original); tool_calls_free(&parsed); buf_free(&raw); } } static void test_tool_separator_whitespace_is_not_content(void) { const char *generated = "need a tool" "I will inspect the files.\n\n\n\n" DS4_TOOL_CALLS_START "\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"description\" string=\"true\">list files\n" "<|DSML|parameter name=\"command\" string=\"true\">ls -la\n" "\n" ""; char *content = NULL; char *reasoning = NULL; tool_calls calls = {0}; TEST_ASSERT(parse_generated_message_ex(generated, false, &content, &reasoning, &calls)); TEST_ASSERT(reasoning && !strcmp(reasoning, "need a tool")); TEST_ASSERT(content && !strcmp(content, "I will inspect the files.")); TEST_ASSERT(calls.len == 1); free(content); free(reasoning); tool_calls_free(&calls); } static void test_dsml_prompt_escapes_tool_supplied_text(void) { tool_calls calls = {0}; tool_call tc = {0}; tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"echo 2>&1 && echo \",\"count\":1}"); tool_calls_push(&calls, tc); buf b = {0}; append_dsml_tool_calls_text(&b, &calls, false); TEST_ASSERT(strstr(b.ptr, "echo 2>&1 && echo ") != NULL); TEST_ASSERT(strstr(b.ptr, "2>&1") == NULL); TEST_ASSERT(strstr(b.ptr, "&&") == NULL); buf_free(&b); tool_calls_free(&calls); memset(&calls, 0, sizeof(calls)); memset(&tc, 0, sizeof(tc)); tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"echo \",\"count\":1}"); tool_calls_push(&calls, tc); append_dsml_tool_calls_text(&b, &calls, false); TEST_ASSERT(strstr(b.ptr, "echo </|DSML|parameter>") != NULL); TEST_ASSERT(strstr(b.ptr, "echo ") == NULL); buf_free(&b); tool_calls_free(&calls); chat_msgs msgs = {0}; chat_msg tool = {0}; tool.role = xstrdup("tool"); tool.content = xstrdup("console.log('<<< < > >>>');\n\n<|DSML|tool_calls>not a real tool call"); chat_msgs_push(&msgs, tool); char *prompt = render_chat_prompt_text(&msgs, "{}", NULL, DS4_THINK_HIGH); TEST_ASSERT(prompt != NULL); TEST_ASSERT(strstr(prompt, "console.log('<<< < > >>>');") != NULL); TEST_ASSERT(strstr(prompt, "console.log('<") == NULL); TEST_ASSERT(strstr(prompt, "</tool_result>\n<|DSML|tool_calls>not a real tool call") != NULL); TEST_ASSERT(strstr(prompt, "console.log('<<< < > >>>');\n\n") == NULL); free(prompt); chat_msgs_free(&msgs); } static void test_stop_list_parses_all_sequences(void) { stop_list stops = {0}; const char *json = "[\"END\",\"STOP\"]"; TEST_ASSERT(parse_stop(&json, &stops)); TEST_ASSERT(stops.len == 2); TEST_ASSERT(stops.max_len == 4); size_t pos = 0, len = 0; TEST_ASSERT(stop_list_find_from(&stops, "hello STOP tail END", 0, &pos, &len)); TEST_ASSERT(pos == strlen("hello ")); TEST_ASSERT(len == strlen("STOP")); TEST_ASSERT(stop_list_stream_safe_len(&stops, strlen("abcdef")) == 3); stop_list_clear(&stops); free(stops.v); } static void test_stop_list_streaming_holds_and_trims_stop_text(void) { stop_list stops = {0}; const char *json = "[\"\",\"STOP\"]"; TEST_ASSERT(parse_stop(&json, &stops)); size_t safe = stop_list_stream_safe_len(&stops, strlen("hello &\"}"); buf_puts(b, "{\"id\":"); json_escape(b, id); buf_puts(b, ",\"type\":\"function\",\"function\":{\"name\":"); json_escape(b, name); buf_puts(b, ",\"arguments\":"); json_escape(b, args.ptr ? args.ptr : ""); buf_puts(b, "}}"); buf_free(&args); } buf_puts(b, "]}"); for (int call = 0; call < 3; call++) { char id[64]; snprintf(id, sizeof(id), "call_%02d_%02d", turn, call); buf_puts(b, ",{\"role\":\"tool\",\"tool_call_id\":"); json_escape(b, id); buf_puts(b, ",\"content\":[{\"type\":\"text\",\"text\":"); json_escape(b, "tool output first line\nsecond line with escaped JSON-looking text {\"ok\":true}"); buf_puts(b, "}]}"); } } buf_putc(b, ']'); } static void test_json_parser_handles_tool_heavy_requests(void) { buf tools = {0}; buf_putc(&tools, '['); for (int i = 0; i < 32; i++) append_tool_heavy_schema(&tools, i); buf_putc(&tools, ']'); buf messages = {0}; append_tool_heavy_messages(&messages); for (int i = 0; i < 32; i++) { const char *tp = tools.ptr; char *schemas = NULL; tool_schema_orders orders = {0}; TEST_ASSERT(parse_tools_value(&tp, &schemas, &orders)); json_ws(&tp); TEST_ASSERT(*tp == '\0'); TEST_ASSERT(schemas && strstr(schemas, "\"name\":\"opencode_tool_00\"")); TEST_ASSERT(tool_schema_orders_find(&orders, "opencode_tool_00") != NULL); free(schemas); tool_schema_orders_free(&orders); const char *mp = messages.ptr; chat_msgs msgs = {0}; TEST_ASSERT(parse_messages(&mp, &msgs)); json_ws(&mp); TEST_ASSERT(*mp == '\0'); TEST_ASSERT(msgs.len == 98); TEST_ASSERT(msgs.v[2].calls.len == 3); TEST_ASSERT(msgs.v[2].calls.v[0].arguments != NULL); TEST_ASSERT(strstr(msgs.v[2].calls.v[0].arguments, "replacement text") != NULL); chat_msgs_free(&msgs); } buf_free(&messages); buf_free(&tools); } static void test_json_string_handles_surrogates(void) { const char *p = "\"paired \\ud83d\\ude80 lone \\ud83d text badlow \\ud83d\\u0041\""; char *s = NULL; TEST_ASSERT(json_string(&p, &s)); TEST_ASSERT(s != NULL); TEST_ASSERT(strstr(s, "paired \xf0\x9f\x9a\x80") != NULL); TEST_ASSERT(strstr(s, "lone \xef\xbf\xbd text") != NULL); TEST_ASSERT(strstr(s, "badlow \xef\xbf\xbd" "A") != NULL); TEST_ASSERT(*p == '\0'); free(s); } static void test_json_int_handles_non_finite_values(void) { const char *p = "NaN"; int value = -1; TEST_ASSERT(json_int(&p, &value)); TEST_ASSERT(value == 0); p = "Infinity"; TEST_ASSERT(json_int(&p, &value)); TEST_ASSERT(value == INT_MAX); p = "-Infinity"; TEST_ASSERT(json_int(&p, &value)); TEST_ASSERT(value == 0); } static void test_tool_history_validation_handles_large_replays(void) { chat_msgs responses = {0}; chat_msgs anthropic = {0}; for (int i = 0; i < 4096; i++) { char id[48]; snprintf(id, sizeof(id), "call_%d", i); chat_msg ra = {.role = xstrdup("assistant"), .reasoning = xstrdup("checked")}; tool_call rtc = {.id = xstrdup(id), .name = xstrdup("run"), .arguments = xstrdup("{}")}; tool_calls_push(&ra.calls, rtc); chat_msgs_push(&responses, ra); chat_msg rt = {.role = xstrdup("tool"), .tool_call_id = xstrdup(id), .content = xstrdup("ok")}; chat_msgs_push(&responses, rt); chat_msg aa = {.role = xstrdup("assistant")}; tool_call atc = {.id = xstrdup(id), .name = xstrdup("run"), .arguments = xstrdup("{}")}; tool_calls_push(&aa.calls, atc); chat_msgs_push(&anthropic, aa); chat_msg at = {.role = xstrdup("user"), .tool_call_id = xstrdup(id), .content = xstrdup("ok")}; chat_msgs_push(&anthropic, at); } char err[160] = {0}; bool needs_live = false; bool needs_reasoning = false; TEST_ASSERT(responses_validate_tool_outputs( NULL, &responses, DS4_THINK_HIGH, &needs_live, &needs_reasoning, err, sizeof(err))); TEST_ASSERT(!needs_live); TEST_ASSERT(!needs_reasoning); TEST_ASSERT(anthropic_validate_tool_results( NULL, &anthropic, &needs_live, err, sizeof(err))); TEST_ASSERT(!needs_live); chat_msgs_free(&responses); chat_msgs_free(&anthropic); } static void test_model_metadata_clamps_completion_to_context(void) { buf b = {0}; append_model_json_values(&b, "deepseek-v4-flash", "DeepSeek V4 Flash", 32768, 393216); TEST_ASSERT(strstr(b.ptr, "\"id\":\"deepseek-v4-flash\"") != NULL); TEST_ASSERT(strstr(b.ptr, "\"name\":\"DeepSeek V4 Flash\"") != NULL); TEST_ASSERT(strstr(b.ptr, "\"context_length\":32768") != NULL); TEST_ASSERT(strstr(b.ptr, "\"max_completion_tokens\":32768") != NULL); TEST_ASSERT(strstr(b.ptr, "\"ignore_eos\"") != NULL); buf_free(&b); append_model_json_values(&b, "deepseek-v4-pro", "DeepSeek V4 Pro", 100000, 4096); TEST_ASSERT(strstr(b.ptr, "\"id\":\"deepseek-v4-pro\"") != NULL); TEST_ASSERT(strstr(b.ptr, "\"name\":\"DeepSeek V4 Pro\"") != NULL); TEST_ASSERT(strstr(b.ptr, "\"context_length\":100000") != NULL); TEST_ASSERT(strstr(b.ptr, "\"max_completion_tokens\":4096") != NULL); buf_free(&b); } static void test_live_prefix_rewind_target(void) { TEST_ASSERT(live_prefix_rewind_target(true, 17, 8, 8) == 7); TEST_ASSERT(live_prefix_rewind_target(true, 49826, 48379, 48379) == 48378); TEST_ASSERT(live_prefix_rewind_target(false, 17, 8, 8) == -1); TEST_ASSERT(live_prefix_rewind_target(true, 17, 8, 7) == -1); TEST_ASSERT(live_prefix_rewind_target(true, 8, 8, 8) == -1); TEST_ASSERT(live_prefix_rewind_target(true, 17, 1, 1) == -1); } static void test_client_socket_nonblocking_flag(void) { int sv[2] = {-1, -1}; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; set_client_socket_nonblocking(sv[0]); int flags = fcntl(sv[0], F_GETFL, 0); TEST_ASSERT(flags >= 0); TEST_ASSERT((flags & O_NONBLOCK) != 0); close(sv[0]); close(sv[1]); } static void test_client_disconnect_probe(void) { int sv[2] = {-1, -1}; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; set_client_socket_nonblocking(sv[0]); TEST_ASSERT(!client_socket_disconnected(sv[0])); TEST_ASSERT(write(sv[1], "x", 1) == 1); TEST_ASSERT(!client_socket_disconnected(sv[0])); char byte = '\0'; TEST_ASSERT(recv(sv[0], &byte, 1, 0) < 0); TEST_ASSERT(errno == EAGAIN || errno == EWOULDBLOCK); close(sv[1]); sv[1] = -1; TEST_ASSERT(client_socket_disconnected(sv[0])); close(sv[0]); TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; set_client_socket_nonblocking(sv[0]); /* Unsupported pipelined input must not hide the FIN behind readable data. */ TEST_ASSERT(write(sv[1], "extra", 5) == 5); close(sv[1]); sv[1] = -1; TEST_ASSERT(client_socket_disconnected(sv[0])); close(sv[0]); TEST_ASSERT(client_poll_revents_disconnected(POLLERR)); TEST_ASSERT(client_poll_revents_disconnected(POLLHUP)); TEST_ASSERT(client_poll_revents_disconnected(POLLNVAL)); TEST_ASSERT(!client_poll_revents_disconnected(POLLIN | POLLOUT)); TEST_ASSERT(client_recv_errno_disconnected(ECONNRESET)); TEST_ASSERT(client_recv_errno_disconnected(EPIPE)); TEST_ASSERT(!client_recv_errno_disconnected(EINTR)); TEST_ASSERT(!client_recv_errno_disconnected(EAGAIN)); TEST_ASSERT(!client_recv_errno_disconnected(EWOULDBLOCK)); } static void test_cancel_job_init(job *j) { memset(j, 0, sizeof(*j)); j->fd = -1; pthread_mutex_init(&j->mu, NULL); pthread_cond_init(&j->cv, NULL); } static void test_cancel_job_destroy(job *j) { pthread_cond_destroy(&j->cv); pthread_mutex_destroy(&j->mu); } static void test_cancelled_progress_callback_is_inert(void) { job j; test_cancel_job_init(&j); job_mark_cancelled(&j); server_prefill_progress progress = { .request_job = &j, .prompt_tokens = 100, .cached_tokens = 10, }; server_progress_cb(&progress, "prefill_chunk", 50, 100); TEST_ASSERT(!progress.seen); TEST_ASSERT(progress.last_current == 0); test_cancel_job_destroy(&j); } static void test_cancel_server_init(server *s) { memset(s, 0, sizeof(*s)); pthread_mutex_init(&s->mu, NULL); pthread_cond_init(&s->cv, NULL); pthread_mutex_init(&s->model_mu, NULL); pthread_cond_init(&s->model_cv, NULL); pthread_mutex_init(&s->tool_mu, NULL); } static void test_cancel_server_destroy(server *s) { pthread_mutex_destroy(&s->tool_mu); pthread_cond_destroy(&s->model_cv); pthread_mutex_destroy(&s->model_mu); pthread_cond_destroy(&s->cv); pthread_mutex_destroy(&s->mu); } typedef struct { server *srv; job *request_job; } test_cancel_wait_arg; static void *test_wait_for_disconnect_main(void *ud) { test_cancel_wait_arg *arg = ud; wait_for_job_or_disconnect(arg->srv, arg->request_job); return NULL; } static void test_waiting_job_cancels_on_client_close(void) { server s; job j; int sv[2] = {-1, -1}; test_cancel_server_init(&s); test_cancel_job_init(&j); TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) { test_cancel_job_destroy(&j); test_cancel_server_destroy(&s); return; } set_client_socket_nonblocking(sv[0]); j.fd = sv[0]; s.head = s.tail = &j; test_cancel_wait_arg arg = {.srv = &s, .request_job = &j}; pthread_t waiter; int thread_rc = pthread_create(&waiter, NULL, test_wait_for_disconnect_main, &arg); TEST_ASSERT(thread_rc == 0); if (thread_rc == 0) { TEST_ASSERT(write(sv[1], "pipelined", 9) == 9); close(sv[1]); sv[1] = -1; TEST_ASSERT(pthread_join(waiter, NULL) == 0); TEST_ASSERT(job_cancelled(&j)); TEST_ASSERT(j.done); TEST_ASSERT(s.head == NULL); TEST_ASSERT(s.tail == NULL); } close(sv[0]); if (sv[1] >= 0) close(sv[1]); test_cancel_job_destroy(&j); test_cancel_server_destroy(&s); } static void test_cancel_unlinks_queued_jobs(void) { server s; job head, middle, tail; test_cancel_server_init(&s); test_cancel_job_init(&head); test_cancel_job_init(&middle); test_cancel_job_init(&tail); head.next = &middle; middle.next = &tail; s.head = &head; s.tail = &tail; server_cancel_job(&s, &middle); TEST_ASSERT(job_cancelled(&middle)); TEST_ASSERT(middle.done); TEST_ASSERT(s.head == &head); TEST_ASSERT(head.next == &tail); TEST_ASSERT(s.tail == &tail); server_cancel_job(&s, &head); TEST_ASSERT(job_cancelled(&head)); TEST_ASSERT(head.done); TEST_ASSERT(s.head == &tail); TEST_ASSERT(s.tail == &tail); server_cancel_job(&s, &tail); TEST_ASSERT(job_cancelled(&tail)); TEST_ASSERT(tail.done); TEST_ASSERT(s.head == NULL); TEST_ASSERT(s.tail == NULL); test_cancel_job_destroy(&tail); test_cancel_job_destroy(&middle); test_cancel_job_destroy(&head); test_cancel_server_destroy(&s); } static void test_cancel_detaches_assigned_job(void) { server s; server_slot slot = {0}; job j; test_cancel_server_init(&s); test_cancel_job_init(&j); s.batched_mode = true; test_server_bind_slot(&s, &slot); slot.assigned = &j; slot.busy = true; server_cancel_job(&s, &j); TEST_ASSERT(job_cancelled(&j)); TEST_ASSERT(j.done); TEST_ASSERT(slot.assigned == NULL); TEST_ASSERT(!slot.busy); test_cancel_job_destroy(&j); test_cancel_server_destroy(&s); } static void test_cancel_running_job_keeps_worker_ownership(void) { server s; server_slot slot = {0}; job j; test_cancel_server_init(&s); test_cancel_job_init(&j); test_server_bind_slot(&s, &slot); slot.running = &j; server_cancel_job(&s, &j); TEST_ASSERT(job_cancelled(&j)); TEST_ASSERT(!j.done); TEST_ASSERT(slot.running == &j); slot.running = NULL; job_complete(&j); TEST_ASSERT(j.done); test_cancel_job_destroy(&j); test_cancel_server_destroy(&s); } static void test_cancel_withdraws_only_pending_decode(void) { server s; server_slot slot = {0}; job pending, in_flight; test_cancel_server_init(&s); test_cancel_job_init(&pending); test_cancel_job_init(&in_flight); s.batched_mode = true; test_server_bind_slot(&s, &slot); slot.running = &pending; slot.decode_pending = true; s.decode_pending = 1; server_cancel_job(&s, &pending); TEST_ASSERT(job_cancelled(&pending)); TEST_ASSERT(!pending.done); TEST_ASSERT(!slot.decode_pending); TEST_ASSERT(slot.decode_done); TEST_ASSERT(slot.decode_rc == DS4_SESSION_SYNC_INTERRUPTED); TEST_ASSERT(s.decode_pending == 0); slot.running = &in_flight; slot.decode_done = false; slot.decode_pending = false; slot.decode_in_flight = true; s.decode_pending = 0; server_cancel_job(&s, &in_flight); TEST_ASSERT(job_cancelled(&in_flight)); TEST_ASSERT(!in_flight.done); TEST_ASSERT(slot.decode_in_flight); TEST_ASSERT(!slot.decode_done); TEST_ASSERT(s.decode_pending == 0); test_cancel_job_destroy(&in_flight); test_cancel_job_destroy(&pending); test_cancel_server_destroy(&s); } static void test_thinking_state_tracks_prompt_and_generated_tags(void) { request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; r.prompt_text = xstrdup("<|Assistant|>"); thinking_state st = thinking_state_from_prompt(&r); TEST_ASSERT(st.inside == true); thinking_state_feed(&st, "reasoning body", strlen("reasoning body")); TEST_ASSERT(st.inside == true); thinking_state_feed(&st, "answer", strlen("nk>answer")); TEST_ASSERT(st.inside == false); thinking_state_feed(&st, "more", strlen("nk>more")); TEST_ASSERT(st.inside == true); request_free(&r); request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_NONE; r.prompt_text = xstrdup("<|Assistant|>"); st = thinking_state_from_prompt(&r); TEST_ASSERT(st.inside == false); request_free(&r); } static void test_thinking_checkpoint_remember_gate(void) { request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; thinking_state st = {.inside = true}; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "length")); TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); st.inside = false; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "length")); TEST_ASSERT(should_remember_thinking_checkpoint(&r, &st, "stop")); r.prompt_preserves_reasoning = true; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); r.prompt_preserves_reasoning = false; r.has_tools = true; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); r.model_syntax = SERVER_MODEL_SYNTAX_QWEN; r.prompt_preserves_reasoning = true; TEST_ASSERT(should_remember_thinking_checkpoint(&r, &st, "stop")); TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "length")); st.inside = true; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); st.inside = false; r.api = API_RESPONSES; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); r.api = API_ANTHROPIC; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); r.has_tools = false; r.think_mode = DS4_THINK_NONE; TEST_ASSERT(!should_remember_thinking_checkpoint(&r, &st, "stop")); request_free(&r); } static void test_tool_marker_state_ignores_orphan_end(void) { bool saw_start = false; bool saw_end = false; bool orphan_end = false; dsml_decode_tracker tracker; dsml_decode_tracker_init(&tracker); observe_tool_markers(&tracker, "reasoning\n" DS4_PARAM_END "\n" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END, &saw_start, &saw_end, &orphan_end); TEST_ASSERT(!saw_start); TEST_ASSERT(!saw_end); TEST_ASSERT(orphan_end); orphan_end = false; const char *prefix = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">"; dsml_decode_tracker_update(&tracker, prefix, strlen(prefix)); observe_tool_markers(&tracker, prefix, &saw_start, &saw_end, &orphan_end); TEST_ASSERT(saw_start); TEST_ASSERT(!saw_end); TEST_ASSERT(!orphan_end); const char *full = DS4_TOOL_CALLS_START "\n" DS4_INVOKE_START " name=\"bash\">" DS4_INVOKE_END "\n" DS4_TOOL_CALLS_END; dsml_decode_tracker_update(&tracker, full, strlen(full)); observe_tool_markers(&tracker, full, &saw_start, &saw_end, &orphan_end); TEST_ASSERT(saw_start); TEST_ASSERT(saw_end); } static void test_canonical_rewrite_rebuilds_when_live_tail_changes(void) { /* Regression for the first canonical-KV rewrite attempt: replacing a small * live suffix looks tempting because the raw SWA ring may still contain the * needed rows, but compressed KV counters and compressor/indexer frontiers * are already past the shared prefix. Until those graph frontiers can be * restored exactly, every rewrite behind the live end must rebuild or load a * disk checkpoint. */ TEST_ASSERT(ds4_session_rewrite_requires_rebuild(19296, 19290, 19081)); TEST_ASSERT(ds4_session_rewrite_requires_rebuild(1024, 1030, 1000)); TEST_ASSERT(ds4_session_rewrite_requires_rebuild(1024, 900, 900)); TEST_ASSERT(!ds4_session_rewrite_requires_rebuild(1024, 1024, 1024)); TEST_ASSERT(!ds4_session_rewrite_requires_rebuild(1024, 1100, 1024)); } static void test_kv_cache_store_len_uses_configured_boundary(void) { kv_disk_cache kc = {0}; kc.opt = kv_cache_default_options(); TEST_ASSERT(kv_cache_store_len(&kc, 11011) == 10240); TEST_ASSERT(kv_cache_store_len(&kc, 1695) == 1695); kc.opt.boundary_trim_tokens = 0; kc.opt.boundary_align_tokens = 1000; TEST_ASSERT(kv_cache_store_len(&kc, 3500) == 3000); kc.opt.boundary_align_tokens = 0; TEST_ASSERT(kv_cache_store_len(&kc, 3500) == 3500); } static void test_kv_cache_chat_anchor_uses_last_user_before_assistant(void) { const int user = 9001; const int assistant = 9002; kv_disk_cache kc = {0}; kc.opt = kv_cache_default_options(); kc.opt.min_tokens = 4; ds4_tokens codex = {0}; ds4_tokens_push(&codex, 1); /* BOS / system */ ds4_tokens_push(&codex, 2); ds4_tokens_push(&codex, user); /* environment_context item */ ds4_tokens_push(&codex, 3); ds4_tokens_push(&codex, 4); ds4_tokens_push(&codex, user); /* actual task starts here */ ds4_tokens_push(&codex, 5); ds4_tokens_push(&codex, assistant); TEST_ASSERT(kv_cache_chat_anchor_pos(&kc, &codex, user, assistant) == 5); ds4_tokens claude = {0}; ds4_tokens_push(&claude, 1); ds4_tokens_push(&claude, 2); ds4_tokens_push(&claude, 3); ds4_tokens_push(&claude, 4); ds4_tokens_push(&claude, user); /* system reminder and task share a turn */ ds4_tokens_push(&claude, 5); ds4_tokens_push(&claude, assistant); TEST_ASSERT(kv_cache_chat_anchor_pos(&kc, &claude, user, assistant) == 4); ds4_tokens_free(&codex); ds4_tokens_free(&claude); } static void test_kv_cache_chat_anchor_ignores_multiturn_tail(void) { const int user = 9001; const int assistant = 9002; kv_disk_cache kc = {0}; kc.opt = kv_cache_default_options(); kc.opt.min_tokens = 2; ds4_tokens prompt = {0}; ds4_tokens_push(&prompt, 1); ds4_tokens_push(&prompt, 2); ds4_tokens_push(&prompt, user); /* first task */ ds4_tokens_push(&prompt, 3); ds4_tokens_push(&prompt, assistant); /* stop scanning here */ ds4_tokens_push(&prompt, 4); ds4_tokens_push(&prompt, user); /* later turn: not a cold anchor */ ds4_tokens_push(&prompt, 5); ds4_tokens_push(&prompt, assistant); TEST_ASSERT(kv_cache_chat_anchor_pos(&kc, &prompt, user, assistant) == 2); kc.opt.min_tokens = 3; TEST_ASSERT(kv_cache_chat_anchor_pos(&kc, &prompt, user, assistant) == -1); TEST_ASSERT(kv_cache_chat_anchor_pos(&kc, &prompt, -1, assistant) == -1); TEST_ASSERT(kv_cache_chat_anchor_pos(&kc, &prompt, user, -1) == -1); ds4_tokens_free(&prompt); } static void test_kv_cache_continued_uses_aligned_frontiers(void) { kv_disk_cache kc = {0}; kc.enabled = true; kc.opt = kv_cache_default_options(); TEST_ASSERT(kv_cache_continued_store_target(&kc, 10239) == 0); TEST_ASSERT(kv_cache_continued_store_target(&kc, 10240) == 10240); kc.continued_last_store_tokens = 4096; TEST_ASSERT(kv_cache_continued_store_target(&kc, 10240) == 10240); kc.continued_last_store_tokens = 24576; TEST_ASSERT(kv_cache_continued_store_target(&kc, 30720) == 30720); kc.continued_last_store_tokens = 10240; TEST_ASSERT(kv_cache_continued_store_target(&kc, 18432) == 0); TEST_ASSERT(kv_cache_continued_store_target(&kc, 20480) == 20480); kc.opt.boundary_align_tokens = 0; kc.continued_last_store_tokens = 20480; TEST_ASSERT(kv_cache_continued_store_target(&kc, 29999) == 0); TEST_ASSERT(kv_cache_continued_store_target(&kc, 30000) == 30000); } static void test_kv_cache_cold_store_suppresses_duplicate_continued_boundary(void) { kv_disk_cache kc = {0}; kc.enabled = true; kc.opt = kv_cache_default_options(); int old = kv_cache_suppress_continued_store(&kc, 10240); TEST_ASSERT(old == 0); TEST_ASSERT(kc.continued_last_store_tokens == 10240); TEST_ASSERT(kv_cache_continued_store_target(&kc, 10240) == 0); kv_cache_restore_suppressed_continued(&kc, old, 10240); TEST_ASSERT(kc.continued_last_store_tokens == 0); TEST_ASSERT(kv_cache_continued_store_target(&kc, 10240) == 10240); } static void test_kv_cache_file_size_must_fit_budget(void) { kv_disk_cache kc = {0}; kc.budget_bytes = 1100; TEST_ASSERT(kv_cache_file_size_fits(&kc, 100, 930, 0, NULL, NULL)); TEST_ASSERT(!kv_cache_file_size_fits(&kc, 100, 938, 0, NULL, NULL)); TEST_ASSERT(!kv_cache_file_size_fits(&kc, 100, 900, 40, NULL, NULL)); TEST_ASSERT(!kv_cache_file_size_fits(&kc, UINT64_MAX, 1, 0, NULL, NULL)); kc.budget_bytes = 0; TEST_ASSERT(kv_cache_file_size_fits(&kc, 100, 900, 40, NULL, NULL)); TEST_ASSERT(!kv_cache_file_size_fits(&kc, UINT64_MAX, 1, 0, NULL, NULL)); } static void test_sha1_bytes_hex_matches_known_vector(void) { char sha[41]; sha1_bytes_hex("abc", 3, sha); TEST_ASSERT(!strcmp(sha, "a9993e364706816aba3e25717850c26c9cd0d89d")); } static void test_kv_stub_file(const char *dir, const char *sha, uint8_t reason, uint32_t tokens, uint32_t hits, uint64_t last_used, uint64_t payload_bytes) { char name[44]; snprintf(name, sizeof(name), "%.40s.kv", sha); char *path = path_join(dir, name); FILE *fp = fopen(path, "wb"); TEST_ASSERT(fp != NULL); if (!fp) { free(path); return; } uint8_t h[KV_CACHE_FIXED_HEADER]; kv_fill_header(h, 2, reason, 0, tokens, hits, 32768, 100, last_used, payload_bytes); uint8_t text_len[4] = {0}; TEST_ASSERT(fwrite(h, 1, sizeof(h), fp) == sizeof(h)); TEST_ASSERT(fwrite(text_len, 1, sizeof(text_len), fp) == sizeof(text_len)); for (uint64_t i = 0; i < payload_bytes; i++) { TEST_ASSERT(fputc(0, fp) != EOF); } TEST_ASSERT(fclose(fp) == 0); free(path); } static void test_kv_text_stub_file_model(const char *dir, const char *text, uint8_t model_id, uint8_t reason, uint32_t tokens, uint64_t payload_bytes) { char sha[41]; sha1_bytes_hex(text, strlen(text), sha); char name[44]; snprintf(name, sizeof(name), "%.40s.kv", sha); char *path = path_join(dir, name); FILE *fp = fopen(path, "wb"); TEST_ASSERT(fp != NULL); if (!fp) { free(path); return; } uint8_t h[KV_CACHE_FIXED_HEADER]; ds4_kvstore_fill_header(h, model_id, 2, reason, 0, tokens, 0, 32768, 100, 100, payload_bytes); uint8_t text_len[4]; le_put32(text_len, (uint32_t)strlen(text)); TEST_ASSERT(fwrite(h, 1, sizeof(h), fp) == sizeof(h)); TEST_ASSERT(fwrite(text_len, 1, sizeof(text_len), fp) == sizeof(text_len)); TEST_ASSERT(fwrite(text, 1, strlen(text), fp) == strlen(text)); for (uint64_t i = 0; i < payload_bytes; i++) { TEST_ASSERT(fputc(0, fp) != EOF); } TEST_ASSERT(fclose(fp) == 0); free(path); } static void test_kv_text_stub_file(const char *dir, const char *text, uint8_t reason, uint32_t tokens, uint64_t payload_bytes) { test_kv_text_stub_file_model(dir, text, 0, reason, tokens, payload_bytes); } static void test_kv_cache_lookup_uses_longest_text_prefix(void) { char tmpl[] = "/tmp/ds4-kv-text-prefix-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *short_text = "transcript prefix"; const char *long_text = "transcript prefix with sampled token bytes"; test_kv_text_stub_file(dir, short_text, KV_REASON_COLD, 512, 0); test_kv_text_stub_file(dir, long_text, KV_REASON_COLD, 768, 0); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); int idx = kv_cache_find_text_prefix(&kc, "transcript prefix with sampled token bytes and suffix", 2, 32768); TEST_ASSERT(idx >= 0); TEST_ASSERT(idx >= 0 && kc.entry[idx].tokens == 768); TEST_ASSERT(idx >= 0 && kc.entry[idx].text_bytes == strlen(long_text)); TEST_ASSERT(kv_cache_find_text_prefix(&kc, "transcript prefiX", 2, 32768) < 0); kv_cache_close(&kc); char short_sha[41], long_sha[41]; sha1_bytes_hex(short_text, strlen(short_text), short_sha); sha1_bytes_hex(long_text, strlen(long_text), long_sha); char short_name[44], long_name[44]; snprintf(short_name, sizeof(short_name), "%.40s.kv", short_sha); snprintf(long_name, sizeof(long_name), "%.40s.kv", long_sha); char *short_path = path_join(dir, short_name); char *long_path = path_join(dir, long_name); unlink(short_path); unlink(long_path); free(short_path); free(long_path); rmdir(dir); } static void test_kv_cache_lookup_rejects_wrong_model(void) { char tmpl[] = "/tmp/ds4-kv-model-id-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *text = "shared rendered prefix"; test_kv_text_stub_file_model(dir, text, 1, KV_REASON_COLD, 512, 0); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); TEST_ASSERT(ds4_kvstore_find_text_prefix(&kc, "shared rendered prefix and tail", 0, 2, 32768) < 0); int idx = ds4_kvstore_find_text_prefix(&kc, "shared rendered prefix and tail", 1, 2, 32768); TEST_ASSERT(idx >= 0); TEST_ASSERT(idx >= 0 && kc.entry[idx].model_id == 1); kv_cache_close(&kc); char sha[41]; sha1_bytes_hex(text, strlen(text), sha); char name[44]; snprintf(name, sizeof(name), "%.40s.kv", sha); char *path = path_join(dir, name); unlink(path); free(path); rmdir(dir); } static void test_kv_cache_lookup_rejects_stale_payload_abi(void) { char tmpl[] = "/tmp/ds4-kv-stale-abi-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *text = "stale rendered prefix"; char sha[41]; sha1_bytes_hex(text, strlen(text), sha); char name[44]; snprintf(name, sizeof(name), "%.40s.kv", sha); char *path = path_join(dir, name); FILE *fp = fopen(path, "wb"); TEST_ASSERT(fp != NULL); if (fp) { uint8_t h[KV_CACHE_FIXED_HEADER]; kv_fill_header(h, 2, KV_REASON_COLD, 0, 512, 0, 32768, 100, 100, 0); h[20] = 0; /* pre-ABI-guard files used this byte as reserved zero. */ uint8_t text_len[4]; le_put32(text_len, (uint32_t)strlen(text)); TEST_ASSERT(fwrite(h, 1, sizeof(h), fp) == sizeof(h)); TEST_ASSERT(fwrite(text_len, 1, sizeof(text_len), fp) == sizeof(text_len)); TEST_ASSERT(fwrite(text, 1, strlen(text), fp) == strlen(text)); TEST_ASSERT(fclose(fp) == 0); } kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); TEST_ASSERT(ds4_kvstore_find_text_prefix(&kc, "stale rendered prefix and tail", 0, 2, 32768) < 0); kv_cache_close(&kc); unlink(path); free(path); rmdir(dir); } static void test_kv_tool_map_filters_by_dsml_text(void) { const char *dsml_keep = "\n\n<|DSML|tool_calls>\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">pwd\n" "\n" ""; const char *dsml_drop = "\n\n<|DSML|tool_calls>\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">zzzz\n" "\n" ""; server src = {0}, dst = {0}; pthread_mutex_init(&src.tool_mu, NULL); pthread_mutex_init(&dst.tool_mu, NULL); tool_memory_put(&src, "call_keep", dsml_keep); tool_memory_put(&src, "call_drop", dsml_drop); FILE *fp = tmpfile(); TEST_ASSERT(fp != NULL); uint64_t estimated_bytes = 0; TEST_ASSERT(kv_tool_map_serialized_size(&src, dsml_keep, &estimated_bytes)); uint64_t bytes = 0; TEST_ASSERT(kv_tool_map_write(&src, fp, dsml_keep, &bytes)); TEST_ASSERT(bytes > 0); TEST_ASSERT(estimated_bytes == bytes); rewind(fp); TEST_ASSERT(kv_tool_map_load_from_pos(&dst, fp, NULL) == 1); chat_msgs msgs = {0}; chat_msg a = {0}; a.role = xstrdup("assistant"); tool_call keep = {.id = xstrdup("call_keep"), .name = xstrdup("bash"), .arguments = xstrdup("{}")}; tool_calls_push(&a.calls, keep); chat_msgs_push(&msgs, a); chat_msg b = {0}; b.role = xstrdup("assistant"); tool_call drop = {.id = xstrdup("call_drop"), .name = xstrdup("bash"), .arguments = xstrdup("{}")}; tool_calls_push(&b.calls, drop); chat_msgs_push(&msgs, b); tool_replay_stats stats = {0}; tool_memory_attach_to_messages(&dst, &msgs, &stats); TEST_ASSERT(msgs.v[0].calls.raw_tool_text != NULL); TEST_ASSERT(msgs.v[1].calls.raw_tool_text == NULL); TEST_ASSERT(stats.disk == 1); TEST_ASSERT(stats.canonical == 1); TEST_ASSERT(stats.missing_ids == 1); TEST_ASSERT(strstr(msgs.v[0].calls.raw_tool_text, "pwd") != NULL); TEST_ASSERT(strstr(msgs.v[0].calls.raw_tool_text, "zzzz") == NULL); chat_msgs_free(&msgs); if (fp) fclose(fp); tool_memory_free(&src.tool_mem); tool_memory_free(&dst.tool_mem); pthread_mutex_destroy(&src.tool_mu); pthread_mutex_destroy(&dst.tool_mu); } static void test_kv_tool_map_restores_before_prompt_render(void) { char tmpl[] = "/tmp/ds4-kv-tool-map-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *sha = "3333333333333333333333333333333333333333"; char name[44]; snprintf(name, sizeof(name), "%.40s.kv", sha); char *path = path_join(dir, name); const char *dsml = "\n\n<|DSML|tool_calls>\n" "<|DSML|invoke name=\"bash\">\n" "<|DSML|parameter name=\"command\" string=\"true\">echo exact\n" "\n" ""; const char *text = dsml; server src = {0}; pthread_mutex_init(&src.tool_mu, NULL); tool_memory_put(&src, "call_disk", dsml); FILE *fp = fopen(path, "wb"); TEST_ASSERT(fp != NULL); if (fp) { uint8_t h[KV_CACHE_FIXED_HEADER]; kv_fill_header(h, 2, KV_REASON_CONTINUED, KV_EXT_TOOL_MAP, 512, 0, 32768, 100, 100, 0); uint8_t text_len[4]; le_put32(text_len, (uint32_t)strlen(text)); TEST_ASSERT(fwrite(h, 1, sizeof(h), fp) == sizeof(h)); TEST_ASSERT(fwrite(text_len, 1, sizeof(text_len), fp) == sizeof(text_len)); TEST_ASSERT(fwrite(text, 1, strlen(text), fp) == strlen(text)); uint64_t ignored = 0; TEST_ASSERT(kv_tool_map_write(&src, fp, dsml, &ignored)); TEST_ASSERT(fclose(fp) == 0); } server dst = {0}; pthread_mutex_init(&dst.tool_mu, NULL); dst.kv.enabled = true; dst.kv.dir = xstrdup(dir); dst.kv.opt = kv_cache_default_options(); chat_msgs msgs = {0}; chat_msg a = {0}; a.role = xstrdup("assistant"); tool_call tc = {0}; tc.id = xstrdup("call_disk"); tc.name = xstrdup("bash"); tc.arguments = xstrdup("{\"command\":\"echo canonical\"}"); tool_calls_push(&a.calls, tc); chat_msgs_push(&msgs, a); kv_cache_restore_tool_memory_for_messages(&dst, &msgs); tool_replay_stats stats = {0}; tool_memory_attach_to_messages(&dst, &msgs, &stats); TEST_ASSERT(msgs.v[0].calls.raw_tool_text != NULL); TEST_ASSERT(stats.disk == 1); TEST_ASSERT(stats.canonical == 0); char *prompt = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(strstr(prompt, "echo exact") != NULL); TEST_ASSERT(strstr(prompt, "echo canonical") == NULL); free(prompt); chat_msgs_free(&msgs); kv_cache_close(&dst.kv); tool_memory_free(&src.tool_mem); tool_memory_free(&dst.tool_mem); pthread_mutex_destroy(&src.tool_mu); pthread_mutex_destroy(&dst.tool_mu); unlink(path); free(path); rmdir(dir); } static void test_kv_cache_eviction_values_fresh_snapshots(void) { char tmpl[] = "/tmp/ds4-kv-evict-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *old_sha = "1111111111111111111111111111111111111111"; const char *new_sha = "2222222222222222222222222222222222222222"; uint64_t now = (uint64_t)time(NULL); test_kv_stub_file(dir, old_sha, KV_REASON_UNKNOWN, 512, 0, now, 4096); test_kv_stub_file(dir, new_sha, KV_REASON_UNKNOWN, 2048, 0, now, 2048); char old_name[44], new_name[44]; snprintf(old_name, sizeof(old_name), "%.40s.kv", old_sha); snprintf(new_name, sizeof(new_name), "%.40s.kv", new_sha); char *old_path = path_join(dir, old_name); char *new_path = path_join(dir, new_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); kc.budget_bytes = (KV_CACHE_FIXED_HEADER + 4u + 2048u) + 16u; kv_cache_evict(&kc, NULL, 0, NULL); TEST_ASSERT(access(old_path, F_OK) != 0); TEST_ASSERT(access(new_path, F_OK) == 0); kv_cache_close(&kc); unlink(old_path); unlink(new_path); free(old_path); free(new_path); rmdir(dir); } static void test_kv_cache_eviction_prefers_anchor_reason(void) { char tmpl[] = "/tmp/ds4-kv-anchor-reason-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *anchor_sha = "1111111111111111111111111111111111111111"; const char *continued_sha = "2222222222222222222222222222222222222222"; uint64_t now = (uint64_t)time(NULL); test_kv_stub_file(dir, anchor_sha, KV_REASON_COLD, 2048, 0, now, 2048); test_kv_stub_file(dir, continued_sha, KV_REASON_CONTINUED, 2048, 0, now, 2048); char anchor_name[44], continued_name[44]; snprintf(anchor_name, sizeof(anchor_name), "%.40s.kv", anchor_sha); snprintf(continued_name, sizeof(continued_name), "%.40s.kv", continued_sha); char *anchor_path = path_join(dir, anchor_name); char *continued_path = path_join(dir, continued_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); kc.budget_bytes = (KV_CACHE_FIXED_HEADER + 4u + 2048u) + 16u; kv_cache_evict(&kc, NULL, 0, NULL); TEST_ASSERT(access(anchor_path, F_OK) == 0); TEST_ASSERT(access(continued_path, F_OK) != 0); kv_cache_close(&kc); unlink(anchor_path); unlink(continued_path); free(anchor_path); free(continued_path); rmdir(dir); } static void test_kv_cache_eviction_makes_room_before_store(void) { char tmpl[] = "/tmp/ds4-kv-pre-store-evict-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *old_sha = "1111111111111111111111111111111111111111"; uint64_t now = (uint64_t)time(NULL); test_kv_stub_file(dir, old_sha, KV_REASON_COLD, 4096, 0, now, 2048); char old_name[44]; snprintf(old_name, sizeof(old_name), "%.40s.kv", old_sha); char *old_path = path_join(dir, old_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); kc.budget_bytes = (KV_CACHE_FIXED_HEADER + 4u + 4096u) + 16u; kv_cache_evict(&kc, NULL, KV_CACHE_FIXED_HEADER + 4u + 4096u, NULL); TEST_ASSERT(access(old_path, F_OK) != 0); kv_cache_close(&kc); unlink(old_path); free(old_path); rmdir(dir); } static void test_kv_cache_eviction_ignores_oversize_incoming(void) { char tmpl[] = "/tmp/ds4-kv-oversize-store-evict-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *old_sha = "1111111111111111111111111111111111111111"; uint64_t now = (uint64_t)time(NULL); test_kv_stub_file(dir, old_sha, KV_REASON_COLD, 4096, 0, now, 1024); char old_name[44]; snprintf(old_name, sizeof(old_name), "%.40s.kv", old_sha); char *old_path = path_join(dir, old_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); kc.budget_bytes = (KV_CACHE_FIXED_HEADER + 4u + 1024u) + 16u; kv_cache_evict(&kc, NULL, kc.budget_bytes + 1, NULL); TEST_ASSERT(access(old_path, F_OK) == 0); kv_cache_close(&kc); unlink(old_path); free(old_path); rmdir(dir); } static void test_kv_cache_eviction_prefers_superseded_continued_prefix(void) { char tmpl[] = "/tmp/ds4-kv-prefix-evict-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *continued_text = "system: hello world"; const char *cold_text = "different stable prefix"; const char *incoming_text = "system: hello world\nuser: prompt"; test_kv_text_stub_file(dir, continued_text, KV_REASON_CONTINUED, 4096, 2048); test_kv_text_stub_file(dir, cold_text, KV_REASON_COLD, 1024, 2048); char continued_sha[41], cold_sha[41]; sha1_bytes_hex(continued_text, strlen(continued_text), continued_sha); sha1_bytes_hex(cold_text, strlen(cold_text), cold_sha); char continued_name[44], cold_name[44]; snprintf(continued_name, sizeof(continued_name), "%.40s.kv", continued_sha); snprintf(cold_name, sizeof(cold_name), "%.40s.kv", cold_sha); char *continued_path = path_join(dir, continued_name); char *cold_path = path_join(dir, cold_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); uint64_t incoming_bytes = KV_CACHE_FIXED_HEADER + 4u + strlen(incoming_text) + 2048u; kc.budget_bytes = incoming_bytes + KV_CACHE_FIXED_HEADER + 4u + strlen(cold_text) + 2048u; ds4_kvstore_eviction_context incoming = { .text = incoming_text, .text_len = strlen(incoming_text), .model_id = 0, .quant_bits = 2, .ctx_size = 32768, .reject_different_quant = false, }; kv_cache_evict(&kc, NULL, incoming_bytes, &incoming); TEST_ASSERT(access(continued_path, F_OK) != 0); TEST_ASSERT(access(cold_path, F_OK) == 0); kv_cache_close(&kc); unlink(continued_path); unlink(cold_path); free(continued_path); free(cold_path); rmdir(dir); } static void test_kv_cache_eviction_keeps_smaller_context_prefix(void) { char tmpl[] = "/tmp/ds4-kv-prefix-ctx-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *continued_text = "system: hello world"; const char *cold_text = "different stable prefix"; const char *incoming_text = "system: hello world\nuser: prompt"; test_kv_text_stub_file(dir, continued_text, KV_REASON_CONTINUED, 4096, 2048); test_kv_text_stub_file(dir, cold_text, KV_REASON_COLD, 1024, 2048); char continued_sha[41], cold_sha[41]; sha1_bytes_hex(continued_text, strlen(continued_text), continued_sha); sha1_bytes_hex(cold_text, strlen(cold_text), cold_sha); char continued_name[44], cold_name[44]; snprintf(continued_name, sizeof(continued_name), "%.40s.kv", continued_sha); snprintf(cold_name, sizeof(cold_name), "%.40s.kv", cold_sha); char *continued_path = path_join(dir, continued_name); char *cold_path = path_join(dir, cold_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); uint64_t incoming_bytes = KV_CACHE_FIXED_HEADER + 4u + strlen(incoming_text) + 2048u; kc.budget_bytes = incoming_bytes + KV_CACHE_FIXED_HEADER + 4u + strlen(continued_text) + 2048u; ds4_kvstore_eviction_context incoming = { .text = incoming_text, .text_len = strlen(incoming_text), .model_id = 0, .quant_bits = 2, .ctx_size = 65536, .reject_different_quant = false, }; kv_cache_evict(&kc, NULL, incoming_bytes, &incoming); TEST_ASSERT(access(continued_path, F_OK) == 0); TEST_ASSERT(access(cold_path, F_OK) != 0); kv_cache_close(&kc); unlink(continued_path); unlink(cold_path); free(continued_path); free(cold_path); rmdir(dir); } static void test_kv_cache_eviction_score_decays_stale_hits(void) { /* stale: lower tokens-per-byte (e.g. tool-heavy prompt) but boosted by * 10 hits well in the past. fresh: higher tokens-per-byte and zero hits, * just stored. The stale hit bonus decays by inactivity, so fresh wins on * its better baseline even though stale once had more successful hits. */ const uint64_t now = 1000u + 14u * KV_CACHE_HIT_HALF_LIFE_SECONDS; kv_entry stale = {.tokens = 1024, .hits = 10, .file_size = 4096, .last_used = 1000}; kv_entry fresh = {.tokens = 2048, .hits = 0, .file_size = 4096, .last_used = now}; double s_on = kv_entry_eviction_score(&stale, NULL, now, NULL); double f_on = kv_entry_eviction_score(&fresh, NULL, now, NULL); TEST_ASSERT(s_on < f_on); /* A fresh entry's score never decays below its (0+1) * tokens/size floor, * regardless of how old another entry's hit history is. */ TEST_ASSERT(f_on == 1.0 * (double)fresh.tokens / (double)fresh.file_size); } static void test_kv_cache_eviction_decayed_hits_tie_break_by_age(void) { char tmpl[] = "/tmp/ds4-kv-stale-hit-evict-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *old_sha = "1111111111111111111111111111111111111111"; const char *new_sha = "2222222222222222222222222222222222222222"; uint64_t now = (uint64_t)time(NULL); uint64_t stale = now > KV_CACHE_HIT_HALF_LIFE_SECONDS * 14ull ? now - KV_CACHE_HIT_HALF_LIFE_SECONDS * 14ull : 1; test_kv_stub_file(dir, old_sha, KV_REASON_COLD, 2048, 15, stale, 2048); test_kv_stub_file(dir, new_sha, KV_REASON_COLD, 2048, 0, now, 2048); char old_name[44], new_name[44]; snprintf(old_name, sizeof(old_name), "%.40s.kv", old_sha); snprintf(new_name, sizeof(new_name), "%.40s.kv", new_sha); char *old_path = path_join(dir, old_name); char *new_path = path_join(dir, new_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); kc.budget_bytes = (KV_CACHE_FIXED_HEADER + 4u + 2048u) + 16u; kv_cache_evict(&kc, NULL, 0, NULL); TEST_ASSERT(access(old_path, F_OK) != 0); TEST_ASSERT(access(new_path, F_OK) == 0); kv_cache_close(&kc); unlink(old_path); unlink(new_path); free(old_path); free(new_path); rmdir(dir); } static void test_kv_cache_eviction_keeps_aligned_continued_frontiers(void) { char tmpl[] = "/tmp/ds4-kv-live-prefix-test.XXXXXX"; char *dir = mkdtemp(tmpl); TEST_ASSERT(dir != NULL); if (!dir) return; const char *cold_sha = "1111111111111111111111111111111111111111"; const char *continued_sha = "2222222222222222222222222222222222222222"; uint64_t now = (uint64_t)time(NULL); test_kv_stub_file(dir, cold_sha, KV_REASON_COLD, 512, 0, now, 2048); test_kv_stub_file(dir, continued_sha, KV_REASON_CONTINUED, 2048, 0, now, 2048); char cold_name[44], continued_name[44]; snprintf(cold_name, sizeof(cold_name), "%.40s.kv", cold_sha); snprintf(continued_name, sizeof(continued_name), "%.40s.kv", continued_sha); char *cold_path = path_join(dir, cold_name); char *continued_path = path_join(dir, continued_name); kv_disk_cache kc = {0}; kc.enabled = true; kc.dir = xstrdup(dir); kc.opt = kv_cache_default_options(); kc.budget_bytes = (KV_CACHE_FIXED_HEADER + 4u + 2048u) + 16u; kv_cache_evict(&kc, NULL, 0, NULL); TEST_ASSERT(access(cold_path, F_OK) != 0); TEST_ASSERT(access(continued_path, F_OK) == 0); kv_cache_close(&kc); unlink(cold_path); unlink(continued_path); free(cold_path); free(continued_path); rmdir(dir); } static void test_thinking_checkpoint_canonical_matches_future_prompt(void) { /* Simulate: user sends a single message, thinking mode on, no tools. * Model generates reasoning + content. The next request will drop the * reasoning from this turn. Verify that: * prompt_text[:-len("")] + "" + content + "<|eos|>" * equals what render_chat_prompt_text produces for the history. */ chat_msgs prefix_msgs = {0}; chat_msg user1 = {0}; user1.role = xstrdup("user"); user1.content = xstrdup("What is 2+2?"); chat_msgs_push(&prefix_msgs, user1); /* This is what prompt_text looks like for the first generation */ char *prompt_text = render_chat_prompt_text(&prefix_msgs, NULL, NULL, DS4_THINK_HIGH); /* prompt_text should end with */ size_t pt_len = strlen(prompt_text); TEST_ASSERT(pt_len >= 7); TEST_ASSERT(!memcmp(prompt_text + pt_len - 7, "", 7)); /* The model generates: reasoning + + content */ const char *reasoning = "Let me think... 2+2 = 4"; const char *content = "The answer is 4."; /* Build the canonical checkpoint text (what we'd produce after canonicalization) */ buf canonical = {0}; buf_append(&canonical, prompt_text, pt_len - 7); /* strip */ buf_puts(&canonical, ""); buf_puts(&canonical, content); buf_puts(&canonical, "<" "\xef\xbd\x9c" "end" "\xe2\x96\x81" "of" "\xe2\x96\x81" "sentence" "\xef\xbd\x9c" ">"); request r; request_init(&r, REQ_CHAT, 128); r.think_mode = DS4_THINK_HIGH; r.prompt_text = xstrdup(prompt_text); char *visible = build_thinking_visible_text(&r, content); TEST_ASSERT(visible != NULL); TEST_ASSERT(!strcmp(visible, canonical.ptr)); free(visible); request_free(&r); /* Now build what the NEXT request would render: history includes this * assistant message, plus a new user message. Extract just the prefix * up to and including the eos of the assistant turn. */ chat_msgs history_msgs = {0}; chat_msg h_user1 = {0}; h_user1.role = xstrdup("user"); h_user1.content = xstrdup("What is 2+2?"); chat_msgs_push(&history_msgs, h_user1); chat_msg h_asst = {0}; h_asst.role = xstrdup("assistant"); h_asst.reasoning = xstrdup(reasoning); h_asst.content = xstrdup(content); chat_msgs_push(&history_msgs, h_asst); chat_msg h_user2 = {0}; h_user2.role = xstrdup("user"); h_user2.content = xstrdup("Thanks!"); chat_msgs_push(&history_msgs, h_user2); char *future_prompt = render_chat_prompt_text(&history_msgs, NULL, NULL, DS4_THINK_HIGH); /* The future prompt should START with our canonical text */ size_t clen = canonical.len; TEST_ASSERT(strlen(future_prompt) > clen); TEST_ASSERT(!memcmp(future_prompt, canonical.ptr, clen)); /* And what comes after is the new user turn + assistant prefix */ const char *rest = future_prompt + clen; TEST_ASSERT(strstr(rest, "Thanks!") != NULL); TEST_ASSERT(strstr(rest, "") != NULL); /* new turn starts thinking */ /* Verify reasoning is NOT in the future prompt for this turn */ const char *asst_turn = strstr(future_prompt, "<" "\xef\xbd\x9c" "Assistant" "\xef\xbd\x9c" ">"); TEST_ASSERT(asst_turn != NULL); TEST_ASSERT(strstr(future_prompt, reasoning) == NULL); /* reasoning dropped */ free(future_prompt); buf_free(&canonical); free(prompt_text); chat_msgs_free(&prefix_msgs); chat_msgs_free(&history_msgs); } static void test_thinking_canonical_empty_content(void) { /* Edge case: model thinks but produces empty content (e.g. tool-less * thinking where answer is entirely in reasoning). Canonical should * still be valid: prompt_text[:-7] + "<|eos|>" */ chat_msgs msgs = {0}; chat_msg user = {0}; user.role = xstrdup("user"); user.content = xstrdup("Think about life"); chat_msgs_push(&msgs, user); char *prompt_text = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); size_t pt_len = strlen(prompt_text); /* Build canonical with empty content */ buf canonical = {0}; buf_append(&canonical, prompt_text, pt_len - 7); buf_puts(&canonical, ""); /* empty content */ buf_puts(&canonical, "<" "\xef\xbd\x9c" "end" "\xe2\x96\x81" "of" "\xe2\x96\x81" "sentence" "\xef\xbd\x9c" ">"); /* Future prompt with empty content assistant message */ chat_msgs history = {0}; chat_msg h_u = {0}; h_u.role = xstrdup("user"); h_u.content = xstrdup("Think about life"); chat_msgs_push(&history, h_u); chat_msg h_a = {0}; h_a.role = xstrdup("assistant"); h_a.reasoning = xstrdup("Deep thoughts about existence..."); h_a.content = xstrdup(""); chat_msgs_push(&history, h_a); chat_msg h_u2 = {0}; h_u2.role = xstrdup("user"); h_u2.content = xstrdup("Continue"); chat_msgs_push(&history, h_u2); char *future = render_chat_prompt_text(&history, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(strlen(future) > canonical.len); TEST_ASSERT(!memcmp(future, canonical.ptr, canonical.len)); /* reasoning dropped */ TEST_ASSERT(strstr(future, "Deep thoughts") == NULL); free(future); buf_free(&canonical); free(prompt_text); chat_msgs_free(&msgs); chat_msgs_free(&history); } static void test_thinking_canonical_multi_turn(void) { /* Multi-turn: 3 user messages, 2 assistant responses with reasoning. * Both prior assistant turns should have reasoning dropped. * The canonical after the SECOND generation should produce text that * matches the start of a 3rd-turn future prompt. */ chat_msgs turn2_prefix = {0}; chat_msg u1 = {0}; u1.role = xstrdup("user"); u1.content = xstrdup("Hello"); chat_msgs_push(&turn2_prefix, u1); chat_msg a1 = {0}; a1.role = xstrdup("assistant"); a1.reasoning = xstrdup("first reasoning"); a1.content = xstrdup("Hi there"); chat_msgs_push(&turn2_prefix, a1); chat_msg u2 = {0}; u2.role = xstrdup("user"); u2.content = xstrdup("How are you?"); chat_msgs_push(&turn2_prefix, u2); /* prompt_text for the 2nd generation (includes 1st assistant turn) */ char *prompt_text = render_chat_prompt_text(&turn2_prefix, NULL, NULL, DS4_THINK_HIGH); size_t pt_len = strlen(prompt_text); TEST_ASSERT(!memcmp(prompt_text + pt_len - 7, "", 7)); /* 1st turn reasoning is already dropped in this prompt_text */ TEST_ASSERT(strstr(prompt_text, "first reasoning") == NULL); TEST_ASSERT(strstr(prompt_text, "Hi there") != NULL); /* After 2nd generation: canonical drops 2nd reasoning too */ const char *content2 = "I'm doing well"; buf canonical = {0}; buf_append(&canonical, prompt_text, pt_len - 7); buf_puts(&canonical, ""); buf_puts(&canonical, content2); buf_puts(&canonical, "<" "\xef\xbd\x9c" "end" "\xe2\x96\x81" "of" "\xe2\x96\x81" "sentence" "\xef\xbd\x9c" ">"); /* Future: 3rd user message arrives */ chat_msgs future_msgs = {0}; chat_msg fu1 = {0}; fu1.role = xstrdup("user"); fu1.content = xstrdup("Hello"); chat_msgs_push(&future_msgs, fu1); chat_msg fa1 = {0}; fa1.role = xstrdup("assistant"); fa1.reasoning = xstrdup("first reasoning"); fa1.content = xstrdup("Hi there"); chat_msgs_push(&future_msgs, fa1); chat_msg fu2 = {0}; fu2.role = xstrdup("user"); fu2.content = xstrdup("How are you?"); chat_msgs_push(&future_msgs, fu2); chat_msg fa2 = {0}; fa2.role = xstrdup("assistant"); fa2.reasoning = xstrdup("second reasoning"); fa2.content = xstrdup(content2); chat_msgs_push(&future_msgs, fa2); chat_msg fu3 = {0}; fu3.role = xstrdup("user"); fu3.content = xstrdup("Great"); chat_msgs_push(&future_msgs, fu3); char *future = render_chat_prompt_text(&future_msgs, NULL, NULL, DS4_THINK_HIGH); /* Both reasonings dropped */ TEST_ASSERT(strstr(future, "first reasoning") == NULL); TEST_ASSERT(strstr(future, "second reasoning") == NULL); /* Canonical is a prefix of future */ TEST_ASSERT(strlen(future) > canonical.len); TEST_ASSERT(!memcmp(future, canonical.ptr, canonical.len)); free(future); buf_free(&canonical); free(prompt_text); chat_msgs_free(&turn2_prefix); chat_msgs_free(&future_msgs); } static void test_thinking_canonical_with_tools_preserves_reasoning(void) { /* When tools ARE present, reasoning is preserved in re-render. * The toolless thinking live binding should NOT fire (has_tools gate), * and the tool-call replay path handles it. Verify the template * preserves reasoning when tool_context is true. */ const char *tool_schemas = "{\"name\":\"bash\"}"; chat_msgs msgs = {0}; chat_msg u = {0}; u.role = xstrdup("user"); u.content = xstrdup("run ls"); chat_msgs_push(&msgs, u); char *prompt_text = render_chat_prompt_text(&msgs, tool_schemas, NULL, DS4_THINK_HIGH); size_t pt_len = strlen(prompt_text); TEST_ASSERT(!memcmp(prompt_text + pt_len - 7, "", 7)); /* With tools, next render KEEPS reasoning */ chat_msgs history = {0}; chat_msg hu = {0}; hu.role = xstrdup("user"); hu.content = xstrdup("run ls"); chat_msgs_push(&history, hu); chat_msg ha = {0}; ha.role = xstrdup("assistant"); ha.reasoning = xstrdup("I should run bash"); ha.content = xstrdup("Here you go"); chat_msgs_push(&history, ha); chat_msg hu2 = {0}; hu2.role = xstrdup("user"); hu2.content = xstrdup("thanks"); chat_msgs_push(&history, hu2); char *future = render_chat_prompt_text(&history, tool_schemas, NULL, DS4_THINK_HIGH); /* Reasoning IS preserved when tools present */ TEST_ASSERT(strstr(future, "I should run bash") != NULL); TEST_ASSERT(strstr(future, "I should run bash") != NULL); free(future); free(prompt_text); chat_msgs_free(&msgs); chat_msgs_free(&history); } static void test_thinking_canonical_non_thinking_mode_noop(void) { /* When thinking is disabled (deepseek-chat), prompt_text ends with * not . The toolless thinking live binding is a no-op * (early return on memcmp check). */ chat_msgs msgs = {0}; chat_msg u = {0}; u.role = xstrdup("user"); u.content = xstrdup("Hello"); chat_msgs_push(&msgs, u); char *prompt_text = render_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_NONE); size_t pt_len = strlen(prompt_text); /* Should end with , not */ TEST_ASSERT(pt_len >= 8); TEST_ASSERT(!memcmp(prompt_text + pt_len - 8, "", 8)); /* Does NOT end with */ TEST_ASSERT(memcmp(prompt_text + pt_len - 7, "", 7) != 0); free(prompt_text); chat_msgs_free(&msgs); } static const char test_inline_png_base64[] = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFUlEQVR4nGP8z8DQwMDAwMAEIkAYABglAYOd/VRoAAAAAElFTkSuQmCC"; static void test_openai_inline_image_content(void) { buf json = {0}; buf_puts(&json, "[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\"," "\"text\":\"describe \"},{\"type\":\"image_url\"," "\"image_url\":{\"url\":\"data:image/png;base64,"); buf_puts(&json, test_inline_png_base64); buf_puts(&json, "\"}},{\"type\":\"text\",\"text\":\" please\"}]}]"); const char *p = json.ptr; chat_msgs msgs = {0}; TEST_ASSERT(parse_messages(&p, &msgs)); TEST_ASSERT(msgs.len == 1); TEST_ASSERT(msgs.v[0].images.len == 1); TEST_ASSERT(strstr(msgs.v[0].content, "describe ") == msgs.v[0].content); TEST_ASSERT(strstr(msgs.v[0].content, " please") != NULL); TEST_ASSERT(msgs.v[0].images.v[0].encoded_len >= 8); TEST_ASSERT(!memcmp(msgs.v[0].images.v[0].encoded, "\x89PNG\r\n\x1a\n", 8)); chat_msgs_free(&msgs); buf_free(&json); } static void test_http_image_paths_and_urls_are_rejected(void) { const char *cases[] = { "[{\"role\":\"user\",\"content\":[{\"type\":\"image_url\"," "\"image_url\":{\"url\":\"https://example.com/a.png\"}}]}]", "[{\"role\":\"user\",\"content\":[{\"type\":\"image_url\"," "\"image_url\":{\"url\":\"/tmp/a.png\"}}]}]", }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { const char *p = cases[i]; chat_msgs msgs = {0}; TEST_ASSERT(!parse_messages(&p, &msgs)); chat_msgs_free(&msgs); } } static void test_anthropic_inline_image_content(void) { buf json = {0}; buf_puts(&json, "[{\"role\":\"user\",\"content\":[{\"type\":\"image\"," "\"source\":{\"type\":\"base64\",\"media_type\":\"image/png\"," "\"data\":\""); buf_puts(&json, test_inline_png_base64); buf_puts(&json, "\"}},{\"type\":\"text\",\"text\":\"describe\"}]}]"); const char *p = json.ptr; chat_msgs msgs = {0}; TEST_ASSERT(parse_anthropic_messages(&p, &msgs)); TEST_ASSERT(msgs.len == 1); TEST_ASSERT(msgs.v[0].images.len == 1); TEST_ASSERT(strstr(msgs.v[0].content, "describe") != NULL); chat_msgs_free(&msgs); buf_free(&json); } static void test_responses_inline_image_content(void) { buf json = {0}; buf_puts(&json, "[{\"type\":\"input_text\",\"text\":\"describe \"}," "{\"type\":\"input_image\",\"image_url\":\"data:image/png;base64,"); buf_puts(&json, test_inline_png_base64); buf_puts(&json, "\"}]"); const char *p = json.ptr; char *content = NULL; server_image_inputs images = {0}; TEST_ASSERT(parse_responses_content_array_multimodal(&p, &content, &images)); TEST_ASSERT(images.len == 1); TEST_ASSERT(strstr(content, "describe ") == content); free(content); server_image_inputs_free(&images); buf_free(&json); } static void test_visible_image_key(void) { char markers[2][SERVER_IMAGE_MARKER_BYTES] = {"nonce_A", "nonce_B"}; request req = {.image_count = 1, .image_markers = markers}; visible_image_key first, next; char *a = visible_prompt_key(&req, "xnonce_Ay", &first); TEST_ASSERT(a && first.count == 1 && first.offsets[0] == 1); memcpy(markers[0], "nonce_Z", 8); char *b = visible_prompt_key(&req, "xnonce_Zy", &next); TEST_ASSERT(a && b && !strcmp(a, b)); TEST_ASSERT(visible_image_prefix_matches(&next, &first, strlen(a))); free(b); req.image_count = 0; b = visible_prompt_key(&req, a, &next); TEST_ASSERT(b && !strcmp(a, b)); TEST_ASSERT(!visible_image_prefix_matches(&next, &first, strlen(a))); free(b); req.image_count = 2; b = visible_prompt_key(&req, "xnonce_Zy-nextnonce_B", &next); TEST_ASSERT(b && byte_prefix_match(b, strlen(b), a, strlen(a))); TEST_ASSERT(visible_image_prefix_matches(&next, &first, strlen(a))); next.offsets[0]++; TEST_ASSERT(!visible_image_prefix_matches(&next, &first, strlen(a))); free(b); free(a); TEST_ASSERT(visible_prompt_key(&req, "marker missing", &next) == NULL); request glm = {.model_syntax = SERVER_MODEL_SYNTAX_GLM, .think_mode = DS4_THINK_HIGH, .prompt_text = "<|user|>hello<|assistant|>"}; char *visible = build_thinking_visible_text(&glm, " hello "); TEST_ASSERT(!strcmp(visible, "<|user|>hello<|assistant|>hello")); free(visible); } static void test_anthropic_tool_image_output(void) { buf json = {0}; buf_puts(&json, "[{\"content\":[{\"type\":\"tool_result\"," "\"tool_use_id\":\"tool_test\",\"content\":[" "{\"type\":\"text\",\"text\":\"read image\"}," "{\"type\":\"image\",\"source\":{\"type\":\"base64\"," "\"media_type\":\"image/png\",\"data\":\""); buf_puts(&json, test_inline_png_base64); buf_puts(&json, "\"}}]}],\"role\":\"user\"}]"); const char *p = json.ptr; chat_msgs msgs = {0}; TEST_ASSERT(parse_anthropic_messages(&p, &msgs)); TEST_ASSERT(msgs.len == 1 && msgs.v[0].images.len == 1); TEST_ASSERT(strstr(msgs.v[0].content, "read image") != NULL); TEST_ASSERT(strstr(msgs.v[0].content, msgs.v[0].images.v[0].marker) != NULL); chat_msgs_free(&msgs); buf_free(&json); const char *invalid[] = { "[{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"content\":[" "{\"type\":\"tool_use\",\"name\":\"bash\",\"input\":{}}]}]}]", "[{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"content\":[" "{\"type\":\"tool_result\",\"content\":\"nested\"}]}]}]", }; for (size_t i = 0; i < 2; i++) { p = invalid[i]; TEST_ASSERT(!parse_anthropic_messages(&p, &msgs)); chat_msgs_free(&msgs); } } static void test_responses_tool_image_output(void) { const char *types[] = {"function_call_output", "custom_tool_call_output", "reasoning"}; for (size_t i = 0; i < 3; i++) { buf json = {0}; buf_puts(&json, "[{\"type\":\""); buf_puts(&json, types[i]); buf_puts(&json, "\",\"call_id\":\"call_test\",\"output\":[" "{\"type\":\"input_text\",\"text\":\"read image\"}," "{\"type\":\"input_image\",\"image_url\":\"data:image/png;base64,"); buf_puts(&json, test_inline_png_base64); buf_puts(&json, "\"}]}]"); const char *p = json.ptr; chat_msgs msgs = {0}; bool ok = parse_responses_input(&p, &msgs, NULL, NULL); TEST_ASSERT(ok == (i < 2)); if (ok) { TEST_ASSERT(msgs.len == 1 && !strcmp(msgs.v[0].role, "tool")); TEST_ASSERT(msgs.v[0].images.len == 1); TEST_ASSERT(strstr(msgs.v[0].content, msgs.v[0].images.v[0].marker) != NULL); } chat_msgs_free(&msgs); buf_free(&json); } } static void test_server_image_embedding_cache(void) { server_image_cache cache = {0}; uint8_t key = 1; float data[2] = {1.25f, -2.5f}; server_image_input input = {.encoded = &key, .encoded_len = 1}; ds4_vision_embedding src = {.data = data, .token_count = 1, .width = 42, .fingerprint = {7}}; ds4_vision_embedding out = {0}; const size_t budget = 2 * (sizeof(data) + 1); TEST_ASSERT(!server_image_cache_get(&cache, &input, &out)); server_image_cache_put(&cache, &input, &src, 2, budget); TEST_ASSERT(server_image_cache_get(&cache, &input, &out)); TEST_ASSERT(out.data != data && !memcmp(out.data, data, sizeof(data))); TEST_ASSERT(out.width == 42 && out.fingerprint[0] == 7); out.data[0] = 99; ds4_vision_embedding_free(&out); key = 2; TEST_ASSERT(!server_image_cache_get(&cache, &input, &out)); server_image_cache_put(&cache, &input, &src, 2, budget); key = 1; TEST_ASSERT(server_image_cache_get(&cache, &input, &out)); TEST_ASSERT(out.data[0] == data[0]); ds4_vision_embedding_free(&out); key = 3; server_image_cache_put(&cache, &input, &src, 2, budget); TEST_ASSERT(cache.bytes == budget); key = 2; TEST_ASSERT(!server_image_cache_get(&cache, &input, &out)); key = 1; TEST_ASSERT(server_image_cache_get(&cache, &input, &out)); ds4_vision_embedding_free(&out); server_image_cache_clear(&cache); TEST_ASSERT(cache.bytes == 0 && cache.clock == 0); for (key = 1; key <= SERVER_IMAGE_CACHE_ENTRIES + 1; key++) server_image_cache_put(&cache, &input, &src, 2, 4096); TEST_ASSERT(cache.bytes == SERVER_IMAGE_CACHE_ENTRIES * (sizeof(data) + 1)); key = 1; TEST_ASSERT(!server_image_cache_get(&cache, &input, &out)); server_image_cache_clear(&cache); src.token_count = UINT32_MAX; server_image_cache_put(&cache, &input, &src, UINT32_MAX, SIZE_MAX); TEST_ASSERT(cache.bytes == 0); src.token_count = 1; server_image_cache_put(&cache, &input, &src, 2, sizeof(data)); TEST_ASSERT(cache.bytes == 0); /* V4.1 image rows are wider than the older 4096-dimensional encoders. */ float wide[2 * 5120]; for (size_t i = 0; i < sizeof(wide) / sizeof(*wide); i++) wide[i] = (float)i; src.data = wide; src.token_count = 2; server_image_cache_put(&cache, &input, &src, 5120, SERVER_IMAGE_CACHE_BYTES); TEST_ASSERT(cache.bytes == sizeof(wide) + input.encoded_len); const bool found = server_image_cache_get(&cache, &input, &out); TEST_ASSERT(found); if (found) { TEST_ASSERT(out.token_count == 2 && !memcmp(out.data, wide, sizeof(wide))); ds4_vision_embedding_free(&out); } server_image_cache_clear(&cache); } static void test_deepseek41_server_stream(void) { const char *raw = "Plan.\n\n" DS41_TOOL_CALLS_START "\n" DS41_INVOKE_START " name=\"write\">\n" DS41_PARAM_START " name=\"content\" string=\"true\">" "literal </|DSML| parameter> caf\xc3\xa8" DS41_PARAM_END "\n" DS41_INVOKE_END "\n" DS41_TOOL_CALLS_END; for (size_t split = 0; split <= strlen(raw); split++) { int sv[2] = {-1, -1}; TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); if (sv[0] < 0 || sv[1] < 0) return; request r; request_init(&r, REQ_CHAT, 128); r.api = API_OPENAI; r.model_syntax = SERVER_MODEL_SYNTAX_DEEPSEEK41; r.stream = r.has_tools = true; r.think_mode = DS4_THINK_HIGH; openai_stream st; openai_stream_start(&r, &st); char *partial = xstrndup(raw, split); TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_v41", &st, partial, split, false)); free(partial); TEST_ASSERT(openai_sse_stream_update(sv[0], NULL, &r, "chatcmpl_v41", &st, raw, strlen(raw), false)); shutdown(sv[0], SHUT_WR); char *out = read_socket_text(sv[1]); buf arguments = {0}; const char *p = out; while ((p = strstr(p, "\"arguments\":")) != NULL) { p += strlen("\"arguments\":"); char *delta = NULL; TEST_ASSERT(json_string(&p, &delta)); if (!delta) break; buf_puts(&arguments, delta); free(delta); } json_args args = {0}; TEST_ASSERT(arguments.ptr && json_args_parse(arguments.ptr, &args)); int index = json_args_find_unused(&args, "content"); TEST_ASSERT(index >= 0 && !strcmp(args.v[index].value, "literal caf\xc3\xa8")); TEST_ASSERT(strstr(out, "\"name\":\"write\"")); TEST_ASSERT(!strstr(out, DS41_TOOL_CALLS_START)); bool complete = index >= 0 && !strcmp(args.v[index].value, "literal
caf\xc3\xa8"); if (!complete) fprintf(stderr, "V4.1 SSE split=%zu: %s\n", split, out); json_args_free(&args); buf_free(&arguments); free(out); openai_stream_free(&st); request_free(&r); close(sv[0]); close(sv[1]); if (!complete) break; } } static void test_deepseek41_server_tools(void) { tool_calls original = {0}, parsed = {0}; tool_call call = {.name = xstrdup("write"), .arguments = xstrdup("{\"path\":\"a.c\",\"content\":\"literal </|DSML| parameter>\"}")}; tool_calls_push(&original, call); buf raw = {0}; buf_puts(&raw, "Plan."); append_dsml_tool_calls_text(&raw, &original, true); char *content = NULL, *reasoning = NULL; TEST_ASSERT(parse_generated_message_ex_for_syntax(SERVER_MODEL_SYNTAX_DEEPSEEK41, raw.ptr, true, &content, &reasoning, &parsed)); TEST_ASSERT(parsed.len == 1 && content && !content[0]); TEST_ASSERT(reasoning && !strcmp(reasoning, "Plan.")); if (parsed.len == 1) { json_args args = {0}; TEST_ASSERT(json_args_parse(parsed.v[0].arguments, &args)); int index = json_args_find_unused(&args, "content"); TEST_ASSERT(index >= 0 && !strcmp(args.v[index].value, "literal </|DSML| parameter>")); json_args_free(&args); } free(content); free(reasoning); tool_calls_free(&parsed); tool_calls_free(&original); buf_free(&raw); const char *json = "[{\"role\":\"user\",\"content\":\"Hi\"}," "{\"role\":\"assistant\",\"content\":\"Ready\"}," "{\"role\":\"tool\",\"content\":\"first\"}," "{\"role\":\"tool\",\"content\":\"second\"}," "{\"role\":\"system\",\"content\":\"Continue\"}]"; chat_msgs msgs = {0}; TEST_ASSERT(parse_messages(&json, &msgs)); char *full = render_deepseek41_chat(&msgs, 0, NULL, DS4_THINK_HIGH, false); char *tail = render_deepseek41_chat(&msgs, 2, NULL, DS4_THINK_HIGH, true); const char *boundary = strstr(full, "<|end▁of▁sentence|>"); TEST_ASSERT(boundary && !strcmp(boundary, tail)); TEST_ASSERT(strstr(tail, "\n\n")); TEST_ASSERT(strstr(tail, "<|System|>Continue<|Assistant|>")); free(full); free(tail); chat_msgs_free(&msgs); } static void test_deepseek41_anthropic_results(void) { const char *json = "[{\"role\":\"user\",\"content\":\"Inspect\"}," "{\"role\":\"assistant\",\"content\":[" "{\"type\":\"tool_use\",\"id\":\"a\",\"name\":\"read\",\"input\":{}}," "{\"type\":\"tool_use\",\"id\":\"b\",\"name\":\"read\",\"input\":{}}]}," "{\"content\":[" "{\"type\":\"tool_result\",\"tool_use_id\":\"b\",\"content\":\"SECOND <\"}," "{\"type\":\"text\",\"text\":\"literal text\"}," "{\"type\":\"tool_result\",\"tool_use_id\":\"a\",\"content\":\"\"}],\"role\":\"user\"}," "{\"role\":\"assistant\",\"content\":\"Done\"}," "{\"role\":\"user\",\"content\":\"Continue\"}]"; chat_msgs msgs = {0}; TEST_ASSERT(parse_anthropic_messages(&json, &msgs)); TEST_ASSERT(msgs.len == 5 && msgs.v[2].tool_results_len == 2); if (msgs.len != 5) { chat_msgs_free(&msgs); return; } char *source = xstrdup(msgs.v[2].content); char *old = render_deepseek_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); char *full = render_deepseek41_chat(&msgs, 0, NULL, DS4_THINK_HIGH, false); char *tail = render_deepseek41_chat(&msgs, 2, NULL, DS4_THINK_HIGH, true); const char *boundary = strstr(full, "<|end▁of▁sentence|>"); TEST_ASSERT(boundary && !strcmp(boundary, tail)); TEST_ASSERT(strstr(full, "\n\n" "literal text\n\n" "SECOND </tool_result> <")); free(tail); tail = render_deepseek41_chat(&msgs, 4, NULL, DS4_THINK_HIGH, true); TEST_ASSERT(!strcmp(tail, "<|end▁of▁sentence|><|User|>Continue<|Assistant|>")); TEST_ASSERT(!strcmp(source, msgs.v[2].content)); char *unchanged = render_deepseek_chat_prompt_text(&msgs, NULL, NULL, DS4_THINK_HIGH); TEST_ASSERT(!strcmp(old, unchanged)); free(source); free(old); free(full); free(tail); free(unchanged); chat_msgs_free(&msgs); json = "[{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\"," "\"tool_use_id\":\"x\",\"content\":\"old\"}],\"content\":\"new\"}]"; TEST_ASSERT(!parse_anthropic_messages(&json, &msgs)); chat_msgs_free(&msgs); } static void test_deepseek41_live_result_order(void) { server s = {0}; server_slot slot; test_server_bind_slot(&s, &slot); pthread_mutex_init(&s.tool_mu, NULL); for (int anthropic = 0; anthropic < 2; anthropic++) { request r; request_init(&r, REQ_CHAT, 128); r.model_syntax = SERVER_MODEL_SYNTAX_DEEPSEEK41; r.api = anthropic ? API_ANTHROPIC : API_RESPONSES; r.think_mode = DS4_THINK_HIGH; live_tool_state *live = anthropic ? &slot.anthropic_live : &slot.responses_live; live->valid = true; id_list_push_unique(&live->call_ids, "a"); id_list_push_unique(&live->call_ids, "b"); const char *json = anthropic ? "[{\"role\":\"user\",\"content\":[" "{\"type\":\"tool_result\",\"tool_use_id\":\"b\",\"content\":\"SECOND\"}," "{\"type\":\"tool_result\",\"tool_use_id\":\"a\",\"content\":\"FIRST\"}]}]" : "[{\"role\":\"tool\",\"tool_call_id\":\"b\",\"content\":\"SECOND\"}," "{\"role\":\"tool\",\"tool_call_id\":\"a\",\"content\":\"FIRST\"}]"; chat_msgs msgs = {0}; TEST_ASSERT(anthropic ? parse_anthropic_messages(&json, &msgs) : parse_messages(&json, &msgs)); if (anthropic) anthropic_prepare_live_continuation(&s, &r, &msgs); else responses_prepare_live_continuation(&s, &r, &msgs); const char *tail = anthropic ? r.anthropic_live_suffix_text : r.responses_live_suffix_text; TEST_ASSERT(tail && !strcmp(tail, "<|end▁of▁sentence|><|User|>FIRST\n\n" "SECOND<|Assistant|>")); live_tool_state_clear_locked(live); /* Rendered requests own their bytes even if the slot is replaced. */ TEST_ASSERT(tail && strstr(tail, "FIRST\n\nSECOND")); free(live->call_ids.v); memset(&live->call_ids, 0, sizeof(live->call_ids)); chat_msgs_free(&msgs); request_free(&r); } pthread_mutex_destroy(&s.tool_mu); } static void ds4_server_unit_tests_run(void) { test_deepseek41_server_stream(); test_deepseek41_server_tools(); test_deepseek41_anthropic_results(); test_deepseek41_live_result_order(); test_visible_image_key(); test_anthropic_tool_image_output(); test_responses_tool_image_output(); test_server_image_embedding_cache(); test_batched_prefill_round_robin(); test_mixed_prefill_quantum_option(); test_multimodal_prefill_resume_frontier(); test_batched_live_continuation_slot_binding(); test_live_continuation_contract(); test_slot_probe_and_routing_scores(); test_slot_probe_live_state_tiers(); test_slot_probe_vision_tiers(); test_slot_routing_staleness_tiers(); test_dispatch_routes_alien_request_to_empty_slot(); test_request_defaults_use_min_p_filtering(); test_chat_ignore_eos_contract(); test_reasoning_effort_mapping(); test_model_alias_thinking_controls(); test_api_thinking_controls_parse(); test_render_think_max_prompt_prefix(); test_render_non_thinking_prompt_closes_think(); test_render_drops_old_reasoning_without_tools(); test_render_preserves_reasoning_with_tools(); test_render_chat_prompt_text_renders_tools_before_system(); test_render_glm_chat_prompt_text(); test_render_qwen_chat_prompt_text(); test_render_qwen_tool_round_trip(); test_qwen_tool_visible_checkpoint_boundary(); test_qwen_decode_tracker_markers(); test_parse_qwen_tool_call_message(); test_qwen_literal_tool_end_in_argument(); test_qwen_string_arguments_follow_schema(); test_qwen_tool_checkpoint_round_trip(); test_qwen_sampled_tool_text_after_think_renders_exactly(); test_qwen_parallel_tool_calls_parse_and_replay(); test_qwen_plain_answer_keeps_trailing_whitespace(); test_qwen_checkpoint_suffix_matches_render(); test_qwen_thinking_visible_text_matches_render(); test_qwen_reasoning_effort_levels(); test_render_glm_drops_old_reasoning_without_tools(); test_render_glm_preserves_reasoning_with_tools(); test_glm_tool_calls_render_matches_template(); test_render_glm_groups_tool_results(); test_tool_schema_order_from_anthropic_schema(); test_tool_schema_order_from_openai_tools(); test_tool_schema_order_from_responses_tool_search(); test_responses_function_named_tool_search_stays_function_call(); test_responses_namespace_tool_schemas_restore_wire_namespace(); test_responses_input_tool_search_output_loads_tools(); test_responses_input_tool_search_output_rejects_bad_tools(); test_responses_input_function_call_namespace_round_trips_to_dsml(); test_responses_output_sends_tool_search_call_item(); test_dsml_tool_args_preserve_call_order(); test_openai_tool_args_preserve_call_order(); test_anthropic_thinking_and_tool_args_preserve_call_order(); test_context_length_error_uses_protocol_standard_shape(); test_cors_headers_are_opt_in(); test_cors_preflight_response_is_no_content(); test_cors_sse_headers(); test_anthropic_live_stream_sends_incremental_blocks(); test_anthropic_stream_reroutes_second_reasoning_pass(); test_anthropic_usage_reports_cache_details(); test_anthropic_tool_stream_sends_live_tool_use(); test_openai_tool_stream_sends_incremental_text(); test_openai_stream_reroutes_second_reasoning_pass(); test_openai_qwen_tool_stream_sends_answer_before_finish(); test_qwen_stream_split_reasoning_close(); test_openai_stream_usage_reports_cache_details(); test_responses_usage_reports_cache_details(); test_openai_chat_stream_splits_reasoning_without_tools(); test_openai_tool_stream_sends_partial_arguments(); test_openai_glm_tool_stream_suppresses_raw_tool_call(); test_openai_tool_stream_waits_for_incomplete_tool_tags(); test_openai_tool_stream_sends_partial_raw_arguments(); test_openai_tool_stream_preserves_literal_entities(); test_openai_tool_stream_holds_partial_utf8_arguments(); test_openai_tool_stream_handles_multiple_calls(); test_streaming_holds_partial_utf8(); test_parse_short_dsml_and_canonical_suffix(); test_parse_glm_tool_call_message(); test_dsml_parser_recovers_loose_nested_parameters(); test_dsml_repair_produces_parseable_calls(); test_tool_parse_failure_returns_recoverable_finish(); test_invalid_dsml_tool_error_suffix_includes_system_prompt(); test_invalid_glm_tool_error_suffix(); test_tool_recovery_output_budget(); test_incomplete_tool_call_keeps_stop_reason(); test_invalid_qwen_tool_error_suffix(); test_thinking_dsml_is_not_executable_before_think_close(); test_thinking_dsml_after_think_close_is_executable(); test_tool_checkpoint_suffix_is_future_prompt_canonical(); test_glm_tool_checkpoint_suffix_is_canonical(); test_tool_checkpoint_minifies_json_parameters(); test_tool_memory_replays_sampled_dsml(); test_anthropic_tool_memory_replays_sampled_dsml(); test_anthropic_live_tail_renders_tool_results_only(); test_anthropic_tool_result_id_validation(); test_anthropic_full_replay_allows_unknown_live_id(); test_anthropic_tool_use_parses_before_role(); test_tool_checkpoint_canonicalization_gate_exact_replay(); test_responses_live_tail_renders_tool_outputs_only(); test_responses_tool_output_id_validation(); test_responses_stateless_tool_replay_requires_reasoning(); test_responses_visible_suffix_matches_client_replay(); test_exact_dsml_tool_replay_can_be_disabled(); test_dsml_decode_state_separates_structure_and_payload(); test_decode_tracker_split_parameter_close(); test_glm_decode_tracker_boundaries(); test_tool_control_text_inside_arguments(); test_tool_body_escape_round_trip(); test_tool_memory_max_ids_prunes_oldest(); test_kv_tool_map_filters_by_dsml_text(); test_kv_tool_map_restores_before_prompt_render(); test_thinking_checkpoint_canonical_matches_future_prompt(); test_thinking_canonical_empty_content(); test_thinking_canonical_multi_turn(); test_thinking_canonical_with_tools_preserves_reasoning(); test_thinking_canonical_non_thinking_mode_noop(); test_openai_inline_image_content(); test_http_image_paths_and_urls_are_rejected(); test_anthropic_inline_image_content(); test_responses_inline_image_content(); test_tool_separator_whitespace_is_not_content(); test_dsml_prompt_escapes_tool_supplied_text(); test_stop_list_parses_all_sequences(); test_stop_list_streaming_holds_and_trims_stop_text(); test_json_skip_has_nesting_limit(); test_request_parsers_reject_malformed_duplicate_owned_fields(); test_json_parser_handles_tool_heavy_requests(); test_json_string_handles_surrogates(); test_json_int_handles_non_finite_values(); test_tool_history_validation_handles_large_replays(); test_model_metadata_clamps_completion_to_context(); test_live_prefix_rewind_target(); test_client_socket_nonblocking_flag(); test_client_disconnect_probe(); test_cancelled_progress_callback_is_inert(); test_waiting_job_cancels_on_client_close(); test_cancel_unlinks_queued_jobs(); test_cancel_detaches_assigned_job(); test_cancel_running_job_keeps_worker_ownership(); test_cancel_withdraws_only_pending_decode(); test_thinking_state_tracks_prompt_and_generated_tags(); test_thinking_checkpoint_remember_gate(); test_tool_marker_state_ignores_orphan_end(); test_canonical_rewrite_rebuilds_when_live_tail_changes(); test_kv_cache_store_len_uses_configured_boundary(); test_kv_cache_chat_anchor_uses_last_user_before_assistant(); test_kv_cache_chat_anchor_ignores_multiturn_tail(); test_kv_cache_continued_uses_aligned_frontiers(); test_kv_cache_cold_store_suppresses_duplicate_continued_boundary(); test_kv_cache_file_size_must_fit_budget(); test_sha1_bytes_hex_matches_known_vector(); test_kv_cache_lookup_uses_longest_text_prefix(); test_kv_cache_lookup_rejects_wrong_model(); test_kv_cache_lookup_rejects_stale_payload_abi(); test_kv_cache_eviction_values_fresh_snapshots(); test_kv_cache_eviction_prefers_anchor_reason(); test_kv_cache_eviction_makes_room_before_store(); test_kv_cache_eviction_ignores_oversize_incoming(); test_kv_cache_eviction_prefers_superseded_continued_prefix(); test_kv_cache_eviction_keeps_smaller_context_prefix(); test_kv_cache_eviction_score_decays_stale_hits(); test_kv_cache_eviction_decayed_hits_tie_break_by_age(); test_kv_cache_eviction_keeps_aligned_continued_frontiers(); } #ifndef DS4_SERVER_TEST_NO_MAIN int main(void) { ds4_server_unit_tests_run(); if (test_failures) { fprintf(stderr, "ds4-server tests: %d failure(s)\n", test_failures); return 1; } puts("ds4-server tests: ok"); return 0; } #endif #endif