Chat Format Converter — C source
Convert chat transcripts between OpenAI messages[], Anthropic system+messages, Gemini contents[], and plain Markdown. Roles map faithfully, tool calls are preserved where possible, and anything unmappable is flagged — never dropped silently. Runs entirely in your browser.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* chat-format-converter — C polyglot showcase port.
*
* Converts one chat transcript between provider shapes through a shared internal
* message list:
*
* - openai — {"messages": [{"role": "system|user|assistant|tool", ...}]}
* - anthropic — {"system": "...", "messages": [{"role": "user|assistant", ...}]}
* - gemini — {"contents": [{"role": "user|model", "parts": [...]}]}
* - markdown — a plain `**role**: text` transcript
*
* Language: C (C11, standard library only)
* Source: CosmoDev polyglot showcase port of the Chat Format Converter tool,
* ported from src/lib/chatFormatConverter.ts (the canonical
* TypeScript that powers the live tool).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Roles map faithfully (Gemini has no assistant — it is `model`; Anthropic
* system lives top-level). Fields a target format cannot represent (e.g. tool
* calls in Markdown) are flagged as warnings, never dropped silently. C's
* standard library ships no JSON engine, so this file includes a small
* hand-written recursive-descent JSON parser + writer. Object keys are kept in
* an ordered pair array so insertion order survives a round-trip, exactly like
* the TypeScript original. Parse failures return NULL with the message stored
* in the caller's error buffer. Allocation failures abort() — this is display
* source, not a service; OOM policy belongs to the embedder.
*/
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ------------------------------ allocation ------------------------------- */
static void *xmalloc(size_t n) {
void *p = malloc(n ? n : 1);
if (!p) { fprintf(stderr, "chat-format-converter: out of memory\n"); abort(); }
return p;
}
static void *xrealloc(void *p, size_t n) {
void *q = realloc(p, n ? n : 1);
if (!q) { fprintf(stderr, "chat-format-converter: out of memory\n"); abort(); }
return q;
}
static char *xstrdup(const char *s) {
size_t n = strlen(s);
char *p = xmalloc(n + 1);
memcpy(p, s, n + 1);
return p;
}
static char *xstrndup(const char *s, size_t n) {
char *p = xmalloc(n + 1);
memcpy(p, s, n);
p[n] = '\0';
return p;
}
/* ------------------------------ string buffer ----------------------------- */
typedef struct {
char *data;
size_t len, cap;
} StrBuf;
static void sb_init(StrBuf *sb) {
sb->cap = 64;
sb->len = 0;
sb->data = xmalloc(sb->cap);
sb->data[0] = '\0';
}
static void sb_grow(StrBuf *sb, size_t need) {
if (sb->len + need + 1 > sb->cap) {
while (sb->len + need + 1 > sb->cap) sb->cap *= 2;
sb->data = xrealloc(sb->data, sb->cap);
}
}
static void sb_append_n(StrBuf *sb, const char *s, size_t n) {
sb_grow(sb, n);
memcpy(sb->data + sb->len, s, n);
sb->len += n;
sb->data[sb->len] = '\0';
}
static void sb_append(StrBuf *sb, const char *s) { sb_append_n(sb, s, strlen(s)); }
static void sb_appendf(StrBuf *sb, const char *fmt, ...) {
char tmp[256];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(tmp, sizeof tmp, fmt, ap);
va_end(ap);
if (n < 0) return;
if ((size_t) n < sizeof tmp) {
sb_append_n(sb, tmp, (size_t) n);
} else {
char *big = xmalloc((size_t) n + 1);
va_start(ap, fmt);
vsnprintf(big, (size_t) n + 1, fmt, ap);
va_end(ap);
sb_append_n(sb, big, (size_t) n);
free(big);
}
}
/* ------------------------------ JSON engine ------------------------------- */
typedef enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ } JType;
typedef struct JValue JValue;
typedef struct {
char *key;
JValue *val;
} JPair;
struct JValue {
JType type;
int b; /* J_BOOL */
double num; /* J_NUM */
char *str; /* J_STR */
JValue **items; /* J_ARR */
size_t n_items, cap_items;
JPair *pairs; /* J_OBJ — ordered key/value array */
size_t n_pairs, cap_pairs;
};
static JValue *jnew(JType t) {
JValue *v = xmalloc(sizeof *v);
memset(v, 0, sizeof *v);
v->type = t;
return v;
}
static void jfree(JValue *v) {
if (!v) return;
for (size_t i = 0; i < v->n_items; i++) jfree(v->items[i]);
free(v->items);
for (size_t i = 0; i < v->n_pairs; i++) {
free(v->pairs[i].key);
jfree(v->pairs[i].val);
}
free(v->pairs);
free(v->str);
free(v);
}
static void jarr_push(JValue *arr, JValue *item) {
if (arr->n_items == arr->cap_items) {
arr->cap_items = arr->cap_items ? arr->cap_items * 2 : 8;
arr->items = xrealloc(arr->items, arr->cap_items * sizeof *arr->items);
}
arr->items[arr->n_items++] = item;
}
static void jobj_put(JValue *obj, const char *key, JValue *val) {
if (obj->n_pairs == obj->cap_pairs) {
obj->cap_pairs = obj->cap_pairs ? obj->cap_pairs * 2 : 8;
obj->pairs = xrealloc(obj->pairs, obj->cap_pairs * sizeof *obj->pairs);
}
obj->pairs[obj->n_pairs].key = xstrdup(key);
obj->pairs[obj->n_pairs].val = val;
obj->n_pairs++;
}
static JValue *jstr_new(const char *s) {
JValue *v = jnew(J_STR);
v->str = xstrdup(s);
return v;
}
/* First value stored under `key`, NULL when absent. */
static JValue *jget(JValue *v, const char *key) {
if (!v || v->type != J_OBJ) return NULL;
for (size_t i = 0; i < v->n_pairs; i++) {
if (strcmp(v->pairs[i].key, key) == 0) return v->pairs[i].val;
}
return NULL;
}
/* The value of `key` when it is a string, NULL otherwise. */
static const char *jstr(JValue *v, const char *key) {
JValue *r = jget(v, key);
return r && r->type == J_STR ? r->str : NULL;
}
static int jis_obj(JValue *v) { return v && v->type == J_OBJ; }
/* Recursive-descent JSON parser; trailing garbage is an error. */
typedef struct {
const char *s;
size_t pos, len;
char err[192];
} JParser;
static void jerr(JParser *p, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(p->err, sizeof p->err, fmt, ap);
va_end(ap);
}
static int jpeek(JParser *p) { return p->pos < p->len ? (unsigned char) p->s[p->pos] : -1; }
static void jskip_ws(JParser *p) {
int c = jpeek(p);
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
p->pos++;
c = jpeek(p);
}
}
static JValue *jvalue(JParser *p);
static JValue *jliteral(JParser *p, const char *word, JValue *v) {
size_t n = strlen(word);
if (p->pos + n <= p->len && memcmp(p->s + p->pos, word, n) == 0) {
p->pos += n;
return v;
}
jerr(p, "invalid literal at byte %zu", p->pos);
return NULL;
}
/* Escape a string the way JSON.stringify does: the two mandatory escapes, the
* single-char shorthands, and \u00XX for remaining control characters. */
static void write_escaped(StrBuf *out, const char *s) {
sb_append_n(out, "\"", 1);
for (const unsigned char *c = (const unsigned char *) s; *c; c++) {
switch (*c) {
case '"': sb_append(out, "\\\""); break;
case '\\': sb_append(out, "\\\\"); break;
case '\n': sb_append(out, "\\n"); break;
case '\r': sb_append(out, "\\r"); break;
case '\t': sb_append(out, "\\t"); break;
case '\b': sb_append(out, "\\b"); break;
case '\f': sb_append(out, "\\f"); break;
default:
if (*c < 0x20) sb_appendf(out, "\\u%04x", (unsigned) *c);
else sb_append_n(out, (const char *) c, 1);
}
}
sb_append_n(out, "\"", 1);
}
/* Decode one \uXXXX escape, pairing high surrogates with a following low one. */
static unsigned jhex4(JParser *p, int *ok) {
unsigned v = 0;
for (int i = 0; i < 4; i++) {
int c = jpeek(p);
int d;
if (c >= '0' && c <= '9') d = c - '0';
else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
else {
jerr(p, "invalid hex digit at byte %zu", p->pos);
*ok = 0;
return 0;
}
p->pos++;
v = v * 16u + (unsigned) d;
}
return v;
}
static void append_utf8(StrBuf *out, unsigned cp) {
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = 0xFFFD;
if (cp < 0x80) {
sb_append_n(out, (const char[]) { (char) cp }, 1);
} else if (cp < 0x800) {
sb_append_n(out, (const char[]) { (char) (0xC0 | (cp >> 6)), (char) (0x80 | (cp & 0x3F)) }, 2);
} else if (cp < 0x10000) {
sb_append_n(out, (const char[]) { (char) (0xE0 | (cp >> 12)),
(char) (0x80 | ((cp >> 6) & 0x3F)),
(char) (0x80 | (cp & 0x3F)) }, 3);
} else {
sb_append_n(out, (const char[]) { (char) (0xF0 | (cp >> 18)),
(char) (0x80 | ((cp >> 12) & 0x3F)),
(char) (0x80 | ((cp >> 6) & 0x3F)),
(char) (0x80 | (cp & 0x3F)) }, 4);
}
}
static char *jstring(JParser *p) {
p->pos++; /* opening quote */
StrBuf out;
sb_init(&out);
for (;;) {
int c = jpeek(p);
if (c == -1) {
jerr(p, "unterminated string at byte %zu", p->pos);
free(out.data);
return NULL;
}
p->pos++;
if (c == '"') return out.data;
if (c == '\\') {
int esc = jpeek(p);
if (esc == -1) {
jerr(p, "unterminated escape");
free(out.data);
return NULL;
}
p->pos++;
switch (esc) {
case '"': sb_append_n(&out, "\"", 1); break;
case '\\': sb_append_n(&out, "\\", 1); break;
case '/': sb_append_n(&out, "/", 1); break;
case 'b': sb_append_n(&out, "\b", 1); break;
case 'f': sb_append_n(&out, "\f", 1); break;
case 'n': sb_append_n(&out, "\n", 1); break;
case 'r': sb_append_n(&out, "\r", 1); break;
case 't': sb_append_n(&out, "\t", 1); break;
case 'u': {
int ok = 1;
unsigned hi = jhex4(p, &ok);
if (!ok) { free(out.data); return NULL; }
if (hi >= 0xD800 && hi <= 0xDBFF) {
/* High surrogate: combine with a following \uXXXX low surrogate. */
int paired = jpeek(p) == '\\'
&& p->pos + 1 < p->len && p->s[p->pos + 1] == 'u';
if (paired) {
p->pos += 2;
unsigned lo = jhex4(p, &ok);
if (!ok) { free(out.data); return NULL; }
append_utf8(&out, 0x10000u + ((hi - 0xD800u) << 10) + (lo - 0xDC00u));
} else {
append_utf8(&out, 0xFFFD);
}
} else {
append_utf8(&out, hi);
}
break;
}
default:
jerr(p, "invalid escape \\%c at byte %zu", (char) esc, p->pos - 1);
free(out.data);
return NULL;
}
} else {
sb_append_n(&out, (const char[]) { (char) c }, 1);
}
}
}
static JValue *jnumber(JParser *p) {
size_t start = p->pos;
for (;;) {
int c = jpeek(p);
int keep = c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E'
|| (c >= '0' && c <= '9');
if (keep) p->pos++;
else break;
}
char *text = xstrndup(p->s + start, p->pos - start);
char *end = NULL;
double d = strtod(text, &end);
if (!end || *end != '\0' || end == text) {
jerr(p, "invalid number \"%s\" at byte %zu", text, start);
free(text);
return NULL;
}
free(text);
JValue *v = jnew(J_NUM);
v->num = d;
return v;
}
static JValue *jvalue(JParser *p) {
int c = jpeek(p);
if (c == -1) {
jerr(p, "unexpected end of input");
return NULL;
}
if (c == '{') {
p->pos++;
JValue *obj = jnew(J_OBJ);
jskip_ws(p);
if (jpeek(p) == '}') { p->pos++; return obj; }
for (;;) {
jskip_ws(p);
if (jpeek(p) != '"') {
jerr(p, "expected object key at byte %zu", p->pos);
jfree(obj);
return NULL;
}
char *key = jstring(p);
if (!key) { jfree(obj); return NULL; }
jskip_ws(p);
if (jpeek(p) != ':') {
jerr(p, "expected ':' at byte %zu", p->pos);
free(key);
jfree(obj);
return NULL;
}
p->pos++;
jskip_ws(p);
JValue *val = jvalue(p);
if (!val) { free(key); jfree(obj); return NULL; }
if (obj->n_pairs == obj->cap_pairs) {
obj->cap_pairs = obj->cap_pairs ? obj->cap_pairs * 2 : 8;
obj->pairs = xrealloc(obj->pairs, obj->cap_pairs * sizeof *obj->pairs);
}
obj->pairs[obj->n_pairs].key = key;
obj->pairs[obj->n_pairs].val = val;
obj->n_pairs++;
jskip_ws(p);
if (jpeek(p) == ',') { p->pos++; continue; }
if (jpeek(p) == '}') { p->pos++; return obj; }
jerr(p, "expected ',' or '}' at byte %zu", p->pos);
jfree(obj);
return NULL;
}
}
if (c == '[') {
p->pos++;
JValue *arr = jnew(J_ARR);
jskip_ws(p);
if (jpeek(p) == ']') { p->pos++; return arr; }
for (;;) {
jskip_ws(p);
JValue *item = jvalue(p);
if (!item) { jfree(arr); return NULL; }
jarr_push(arr, item);
jskip_ws(p);
if (jpeek(p) == ',') { p->pos++; continue; }
if (jpeek(p) == ']') { p->pos++; return arr; }
jerr(p, "expected ',' or ']' at byte %zu", p->pos);
jfree(arr);
return NULL;
}
}
if (c == '"') {
char *s = jstring(p);
if (!s) return NULL;
JValue *v = jnew(J_STR);
v->str = s;
return v;
}
if (c == 't') {
JValue *v = jnew(J_BOOL);
v->b = 1;
return jliteral(p, "true", v);
}
if (c == 'f') {
JValue *v = jnew(J_BOOL);
v->b = 0;
return jliteral(p, "false", v);
}
if (c == 'n') return jliteral(p, "null", jnew(J_NULL));
if (c == '-' || (c >= '0' && c <= '9')) return jnumber(p);
jerr(p, "unexpected '%c' at byte %zu", (char) c, p->pos);
return NULL;
}
static JValue *json_parse(const char *input, char *err, size_t errsz) {
JParser p = { .s = input, .pos = 0, .len = strlen(input), .err = "" };
jskip_ws(&p);
JValue *v = jvalue(&p);
if (!v) {
snprintf(err, errsz, "%s", p.err);
return NULL;
}
jskip_ws(&p);
if (p.pos != p.len) {
snprintf(err, errsz, "unexpected trailing characters at byte %zu", p.pos);
jfree(v);
return NULL;
}
return v;
}
/* Render a double the way JavaScript's number-to-string coercion does: whole
* values carry no trailing ".0". */
static void format_f64(StrBuf *out, double n) {
if (n == (double) (long long) n && (n < 0 ? -n : n) < 1e15) {
sb_appendf(out, "%lld", (long long) n);
} else {
sb_appendf(out, "%.15g", n);
}
}
static void push_indent(StrBuf *out, int depth) {
for (int i = 0; i < depth; i++) sb_append(out, " ");
}
static void write_json(StrBuf *out, JValue *v, int indent, int depth) {
switch (v->type) {
case J_NULL:
sb_append(out, "null");
break;
case J_BOOL:
sb_append(out, v->b ? "true" : "false");
break;
case J_NUM:
format_f64(out, v->num);
break;
case J_STR:
write_escaped(out, v->str);
break;
case J_ARR:
if (v->n_items == 0) { sb_append(out, "[]"); break; }
sb_append_n(out, "[", 1);
for (size_t i = 0; i < v->n_items; i++) {
if (i > 0) sb_append_n(out, ",", 1);
if (indent) { sb_append_n(out, "\n", 1); push_indent(out, depth + 1); }
write_json(out, v->items[i], indent, depth + 1);
}
if (indent) { sb_append_n(out, "\n", 1); push_indent(out, depth); }
sb_append_n(out, "]", 1);
break;
case J_OBJ:
if (v->n_pairs == 0) { sb_append(out, "{}"); break; }
sb_append_n(out, "{", 1);
for (size_t i = 0; i < v->n_pairs; i++) {
if (i > 0) sb_append_n(out, ",", 1);
if (indent) { sb_append_n(out, "\n", 1); push_indent(out, depth + 1); }
write_escaped(out, v->pairs[i].key);
sb_append(out, indent ? ": " : ":");
write_json(out, v->pairs[i].val, indent, depth + 1);
}
if (indent) { sb_append_n(out, "\n", 1); push_indent(out, depth); }
sb_append_n(out, "}", 1);
break;
}
}
/* Compact encoding, the moral equivalent of TS `JSON.stringify`. */
static char *jto_compact(JValue *v) {
StrBuf out;
sb_init(&out);
write_json(&out, v, 0, 0);
return out.data;
}
/* Pretty encoding with a 2-space indent, like `JSON.stringify(v, null, 2)`. */
static char *jto_pretty(JValue *v) {
StrBuf out;
sb_init(&out);
write_json(&out, v, 2, 0);
return out.data;
}
/* ------------------------------ domain types ------------------------------ */
/* One assistant-emitted tool call, provider-agnostic. `args` is a JSON string. */
typedef struct {
char *id;
char *name;
char *args;
} ToolCall;
/* The shared internal shape every format parses into and serializes from. */
typedef struct {
char *role; /* "system" | "user" | "assistant" | "tool" */
char *content; /* "" when the message only carries tool calls/results */
char *name; /* nullable */
ToolCall *tool_calls;
size_t n_tool_calls;
int has_tool_calls; /* distinguishes "no field" from "empty list" */
char *tool_call_id; /* nullable */
} Message;
/* The conversion output plus human-readable notes about unmapped fields. */
typedef struct {
char *output;
char **warnings;
size_t n_warnings;
} ConversionResult;
static const char *const CHAT_FORMATS[] = { "openai", "anthropic", "gemini", "markdown" };
/* Shared context: error message + warning accumulator. */
typedef struct {
char err[512];
char **warnings;
size_t n_warnings, cap_warnings;
} Ctx;
static void ctx_warn(Ctx *ctx, const char *fmt, ...) {
if (ctx->n_warnings == ctx->cap_warnings) {
ctx->cap_warnings = ctx->cap_warnings ? ctx->cap_warnings * 2 : 8;
ctx->warnings = xrealloc(ctx->warnings, ctx->cap_warnings * sizeof *ctx->warnings);
}
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
ctx->warnings[ctx->n_warnings++] = xstrdup(buf);
}
static void *ctx_fail(Ctx *ctx, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(ctx->err, sizeof ctx->err, fmt, ap);
va_end(ap);
return NULL;
}
/* ------------------------------ str vectors ------------------------------- */
typedef struct {
char **items;
size_t n, cap;
} StrList;
static void sl_init(StrList *l) { l->items = NULL; l->n = l->cap = 0; }
static void sl_push(StrList *l, char *s) {
if (l->n == l->cap) {
l->cap = l->cap ? l->cap * 2 : 8;
l->items = xrealloc(l->items, l->cap * sizeof *l->items);
}
l->items[l->n++] = s;
}
static void sl_free(StrList *l) {
for (size_t i = 0; i < l->n; i++) free(l->items[i]);
free(l->items);
}
static char *sl_join(StrList *l, const char *sep) {
StrBuf out;
sb_init(&out);
for (size_t i = 0; i < l->n; i++) {
if (i > 0) sb_append(&out, sep);
sb_append(&out, l->items[i]);
}
return out.data;
}
/* -------------------------------- messages -------------------------------- */
typedef struct {
Message *items;
size_t n, cap;
} MsgList;
static void ml_init(MsgList *l) { l->items = NULL; l->n = l->cap = 0; }
static Message *ml_push(MsgList *l, Message m) {
if (l->n == l->cap) {
l->cap = l->cap ? l->cap * 2 : 8;
l->items = xrealloc(l->items, l->cap * sizeof *l->items);
}
l->items[l->n] = m;
return &l->items[l->n++];
}
static void ml_free(MsgList *l) {
for (size_t i = 0; i < l->n; i++) {
Message *m = &l->items[i];
free(m->role);
free(m->content);
free(m->name);
for (size_t k = 0; k < m->n_tool_calls; k++) {
free(m->tool_calls[k].id);
free(m->tool_calls[k].name);
free(m->tool_calls[k].args);
}
free(m->tool_calls);
free(m->tool_call_id);
}
free(l->items);
}
static Message msg_new(const char *role, char *content) {
Message m;
memset(&m, 0, sizeof m);
m.role = xstrdup(role);
m.content = content;
return m;
}
/* -------------------------------- helpers --------------------------------- */
static JValue *parse_transcript_json(Ctx *ctx, const char *text, const char *label) {
char jerr[192];
JValue *v = json_parse(text, jerr, sizeof jerr);
if (!v) return ctx_fail(ctx, "%s: invalid JSON — %s", label, jerr);
return v;
}
/* The TS `?? {}` capture: null/missing becomes {}, any other value passes
* through and stringifies as-is. */
static char *args_of(JValue *v) {
if (!v || v->type == J_NULL) return xstrdup("{}");
return jto_compact(v);
}
/* Parse a JSON-string argument back to a value, an empty object when it is
* not a JSON object (mirrors parseArgsOrEmpty). */
static JValue *parse_args_or_empty(const char *args) {
char jerr[192];
JValue *v = json_parse(args, jerr, sizeof jerr);
if (v && v->type == J_OBJ) return v;
jfree(v);
return jnew(J_OBJ);
}
static const char *block_type_name(JValue *raw) {
const char *t = jstr(raw, "type");
return t ? t : "content";
}
/* One normalized Anthropic content block. */
typedef enum { B_TEXT, B_TOOL_USE, B_TOOL_RESULT, B_OTHER } BKind;
typedef struct {
BKind kind;
char *text; /* B_TEXT */
JValue *raw; /* B_TOOL_USE | B_TOOL_RESULT | B_OTHER */
} Block;
typedef struct {
Block *items;
size_t n, cap;
} BlockList;
static BlockList content_blocks(Ctx *ctx, JValue *content, const char *where) {
BlockList blocks = { NULL, 0, 0 };
if (!content) {
ctx_fail(ctx, "%s: content must be a string or an array of blocks", where);
return blocks;
}
if (content->type == J_STR) {
blocks.items = xmalloc(sizeof *blocks.items);
blocks.items[0] = (Block) { .kind = B_TEXT, .text = xstrdup(content->str) };
blocks.n = 1;
return blocks;
}
if (content->type != J_ARR) {
ctx_fail(ctx, "%s: content must be a string or an array of blocks", where);
return blocks;
}
for (size_t i = 0; i < content->n_items; i++) {
JValue *block = content->items[i];
const char *type = jstr(block, "type");
JValue *textv = jget(block, "text");
int is_text = type && strcmp(type, "text") == 0 && textv && textv->type == J_STR;
Block b;
if (is_text) {
b = (Block) { .kind = B_TEXT, .text = xstrdup(textv->str) };
} else if (type && strcmp(type, "tool_use") == 0) {
b = (Block) { .kind = B_TOOL_USE, .raw = block };
} else if (type && strcmp(type, "tool_result") == 0) {
b = (Block) { .kind = B_TOOL_RESULT, .raw = block };
} else {
b = (Block) { .kind = B_OTHER, .raw = block };
}
if (blocks.n == blocks.cap) {
blocks.cap = blocks.cap ? blocks.cap * 2 : 8;
blocks.items = xrealloc(blocks.items, blocks.cap * sizeof *blocks.items);
}
blocks.items[blocks.n++] = b;
}
return blocks;
}
static void blocks_free(BlockList *blocks) {
for (size_t i = 0; i < blocks->n; i++) free(blocks->items[i].text);
free(blocks->items);
}
/* Anthropic string-or-block[] content → plain text. */
static char *blocks_to_text(Ctx *ctx, JValue *content, const char *where) {
if (!content) return xstrdup("");
if (content->type == J_STR) return xstrdup(content->str);
if (content->type == J_NULL) return xstrdup("");
StrList texts;
sl_init(&texts);
BlockList blocks = content_blocks(ctx, content, where);
if (ctx->err[0]) return NULL;
for (size_t i = 0; i < blocks.n; i++) {
Block b = blocks.items[i];
switch (b.kind) {
case B_TEXT:
sl_push(&texts, xstrdup(b.text));
break;
case B_OTHER:
ctx_warn(ctx, "%s: dropped unsupported %s block", where, block_type_name(b.raw));
break;
case B_TOOL_USE:
ctx_warn(ctx, "%s: dropped tool_use block from text-only content", where);
break;
case B_TOOL_RESULT:
ctx_warn(ctx, "%s: dropped tool_result block from text-only content", where);
break;
}
}
blocks_free(&blocks);
char *joined = sl_join(&texts, "\n");
sl_free(&texts);
return joined;
}
static ToolCall tool_call_from_block(JValue *raw) {
const char *id = jstr(raw, "id");
const char *name = jstr(raw, "name");
return (ToolCall) {
.id = xstrdup(id ? id : ""),
.name = xstrdup(name ? name : ""),
.args = args_of(jget(raw, "input")),
};
}
static int is_blank(const char *s) {
for (; *s; s++) {
if (*s != ' ' && *s != '\t' && *s != '\r' && *s != '\n' && *s != '\x0b' && *s != '\x0c') return 0;
}
return 1;
}
static char *trim_dup(const char *s) {
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n' || *s == '\x0b' || *s == '\x0c') s++;
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t' || s[n - 1] == '\r'
|| s[n - 1] == '\n' || s[n - 1] == '\x0b' || s[n - 1] == '\x0c')) n--;
return xstrndup(s, n);
}
/* ---------------------------------- parse --------------------------------- */
/* OpenAI content → plain text (string, or text parts joined; other parts flagged). */
static char *content_text(Ctx *ctx, JValue *content, const char *where) {
if (!content || content->type == J_NULL) return xstrdup("");
if (content->type == J_STR) return xstrdup(content->str);
if (content->type == J_ARR) {
StrList texts;
sl_init(&texts);
size_t dropped = 0;
for (size_t i = 0; i < content->n_items; i++) {
JValue *part = content->items[i];
const char *type = jstr(part, "type");
JValue *textv = jget(part, "text");
if (type && strcmp(type, "text") == 0 && textv && textv->type == J_STR) {
sl_push(&texts, xstrdup(textv->str));
} else {
dropped++;
}
}
if (dropped > 0) ctx_warn(ctx, "%s: dropped %zu non-text content part(s)", where, dropped);
StrBuf joined;
sb_init(&joined);
for (size_t i = 0; i < texts.n; i++) sb_append(&joined, texts.items[i]);
sl_free(&texts);
return joined.data;
}
return ctx_fail(ctx, "%s: content must be a string, an array of parts, or null", where);
}
static int parse_openai_tool_calls(Ctx *ctx, JValue *raw, ToolCall **out, size_t *n_out,
int *has_out) {
*out = NULL;
*n_out = 0;
if (!raw || raw->type == J_NULL) {
*has_out = 0;
return 0;
}
if (raw->type != J_ARR) {
ctx_fail(ctx, "tool_calls must be an array");
return -1;
}
*has_out = 1;
if (raw->n_items == 0) return 0;
*out = xmalloc(raw->n_items * sizeof **out);
for (size_t i = 0; i < raw->n_items; i++) {
JValue *entry = raw->items[i];
JValue *fn = jget(entry, "function");
const char *id = jstr(entry, "id");
const char *name = fn ? jstr(fn, "name") : NULL;
const char *arguments = fn ? jstr(fn, "arguments") : NULL;
(*out)[i] = (ToolCall) {
.id = xstrdup(id ? id : ""),
.name = xstrdup(name ? name : ""),
.args = xstrdup(arguments ? arguments : ""),
};
}
*n_out = raw->n_items;
return 0;
}
/* OpenAI messages[] (bare array, or wrapped in {"messages": [...]}). */
static int parse_openai(Ctx *ctx, const char *text, MsgList *out) {
JValue *parsed = parse_transcript_json(ctx, text, "OpenAI transcript");
if (!parsed) return -1;
JValue *messages = jis_obj(parsed) ? jget(parsed, "messages") : NULL;
JValue *items = messages && messages->type == J_ARR ? messages
: parsed->type == J_ARR ? parsed : NULL;
if (!items) {
jfree(parsed);
ctx_fail(ctx,
"OpenAI transcript must be a messages[] array or an object with a \"messages\" array");
return -1;
}
int rc = -1;
for (size_t i = 0; i < items->n_items; i++) {
JValue *entry = items->items[i];
char where[64];
snprintf(where, sizeof where, "messages[%zu]", i);
if (!jis_obj(entry)) {
ctx_fail(ctx, "messages[%zu] is not an object", i);
goto done;
}
const char *role = jstr(entry, "role");
char *content = NULL;
if (role && (strcmp(role, "system") == 0 || strcmp(role, "developer") == 0)) {
content = content_text(ctx, jget(entry, "content"), where);
if (!content) goto done;
ml_push(out, msg_new("system", content));
continue;
}
if (role && (strcmp(role, "user") == 0 || strcmp(role, "assistant") == 0
|| strcmp(role, "tool") == 0)) {
content = content_text(ctx, jget(entry, "content"), where);
if (!content) goto done;
Message m = msg_new(role, content);
const char *name = jstr(entry, "name");
const char *tcid = jstr(entry, "tool_call_id");
if (name) m.name = xstrdup(name);
if (tcid) m.tool_call_id = xstrdup(tcid);
if (parse_openai_tool_calls(ctx, jget(entry, "tool_calls"),
&m.tool_calls, &m.n_tool_calls, &m.has_tool_calls) != 0) {
free(m.role);
free(m.content);
goto done;
}
ml_push(out, m);
continue;
}
JValue *rv = jget(entry, "role");
char *rendered = rv ? jto_compact(rv) : NULL;
ctx_fail(ctx, "messages[%zu] has unsupported role: %s", i, rendered ? rendered : "null");
free(rendered);
goto done;
}
rc = 0;
done:
jfree(parsed);
return rc;
}
static void flush_user_texts(MsgList *out, StrList *texts) {
if (texts->n > 0) {
char *joined = sl_join(texts, "\n");
ml_push(out, msg_new("user", joined));
for (size_t i = 0; i < texts->n; i++) free(texts->items[i]);
texts->n = 0;
}
}
/* Anthropic messages[] plus the top-level system field. */
static int parse_anthropic(Ctx *ctx, const char *text, MsgList *out) {
JValue *root = parse_transcript_json(ctx, text, "Anthropic transcript");
if (!root) return -1;
int rc = -1;
JValue *messages = jis_obj(root) ? jget(root, "messages") : NULL;
if (!messages || messages->type != J_ARR) {
ctx_fail(ctx, "Anthropic transcript must be an object with a \"messages\" array");
goto done;
}
if (jget(root, "system")) {
char *sys = blocks_to_text(ctx, jget(root, "system"), "system");
if (!sys) goto done;
ml_push(out, msg_new("system", sys));
}
for (size_t i = 0; i < messages->n_items; i++) {
JValue *entry = messages->items[i];
char where[64];
snprintf(where, sizeof where, "messages[%zu]", i);
if (!jis_obj(entry)) {
ctx_fail(ctx, "messages[%zu] is not an object", i);
goto done;
}
const char *role = jstr(entry, "role");
if (role && strcmp(role, "assistant") == 0) {
StrList texts;
sl_init(&texts);
ToolCall *calls = NULL;
size_t n_calls = 0;
BlockList blocks = content_blocks(ctx, jget(entry, "content"), where);
if (ctx->err[0]) { blocks_free(&blocks); sl_free(&texts); goto done; }
for (size_t k = 0; k < blocks.n; k++) {
Block b = blocks.items[k];
if (b.kind == B_TEXT) {
sl_push(&texts, xstrdup(b.text));
} else if (b.kind == B_TOOL_USE) {
calls = xrealloc(calls, (n_calls + 1) * sizeof *calls);
calls[n_calls++] = tool_call_from_block(b.raw);
} else if (b.kind == B_OTHER) {
ctx_warn(ctx, "%s: dropped unsupported %s block", where, block_type_name(b.raw));
} /* B_TOOL_RESULT: ignored inside an assistant message */
}
blocks_free(&blocks);
char *joined = sl_join(&texts, "\n");
Message m = msg_new("assistant", joined);
sl_free(&texts);
if (n_calls > 0) {
m.tool_calls = calls;
m.n_tool_calls = n_calls;
m.has_tool_calls = 1;
} else {
free(calls);
}
ml_push(out, m);
continue;
}
if (role && strcmp(role, "user") == 0) {
StrList texts;
sl_init(&texts);
BlockList blocks = content_blocks(ctx, jget(entry, "content"), where);
if (ctx->err[0]) { blocks_free(&blocks); sl_free(&texts); goto done; }
for (size_t k = 0; k < blocks.n; k++) {
Block b = blocks.items[k];
if (b.kind == B_TEXT) {
sl_push(&texts, xstrdup(b.text));
continue;
}
if (b.kind == B_TOOL_USE) {
ctx_warn(ctx, "%s: tool_use block inside a user message moved to an assistant tool call", where);
flush_user_texts(out, &texts);
Message m = msg_new("assistant", xstrdup(""));
m.tool_calls = xmalloc(sizeof *m.tool_calls);
m.tool_calls[0] = tool_call_from_block(b.raw);
m.n_tool_calls = 1;
m.has_tool_calls = 1;
ml_push(out, m);
continue;
}
if (b.kind == B_TOOL_RESULT) {
flush_user_texts(out, &texts);
char *ctext = blocks_to_text(ctx, jget(b.raw, "content"), where);
if (!ctext) { blocks_free(&blocks); sl_free(&texts); goto done; }
Message m = msg_new("tool", ctext);
const char *tuid = jstr(b.raw, "tool_use_id");
m.tool_call_id = xstrdup(tuid ? tuid : "");
ml_push(out, m);
continue;
}
ctx_warn(ctx, "%s: dropped unsupported %s block", where, block_type_name(b.raw));
}
blocks_free(&blocks);
flush_user_texts(out, &texts);
sl_free(&texts);
continue;
}
JValue *rv = jget(entry, "role");
char *rendered = rv ? jto_compact(rv) : NULL;
ctx_fail(ctx, "%s has unsupported role: %s", where, rendered ? rendered : "null");
free(rendered);
goto done;
}
rc = 0;
done:
jfree(root);
return rc;
}
/* Gemini systemInstruction (string or {parts}) → plain text. */
static char *gemini_text(Ctx *ctx, JValue *v, const char *where) {
if (v->type == J_STR) return xstrdup(v->str);
if (v->type == J_OBJ) {
const char *t = jstr(v, "text");
if (t) return xstrdup(t);
JValue *parts = jget(v, "parts");
if (parts && parts->type == J_ARR) {
StrList texts;
sl_init(&texts);
for (size_t i = 0; i < parts->n_items; i++) {
const char *pt = jstr(parts->items[i], "text");
sl_push(&texts, xstrdup(pt ? pt : ""));
}
char *joined = sl_join(&texts, "\n");
sl_free(&texts);
return joined;
}
}
ctx_warn(ctx, "%s: unsupported systemInstruction shape, treated as empty", where);
return xstrdup("");
}
/* Gemini contents[] plus the optional systemInstruction. */
static int parse_gemini(Ctx *ctx, const char *text, MsgList *out) {
JValue *root = parse_transcript_json(ctx, text, "Gemini transcript");
if (!root) return -1;
int rc = -1;
JValue *contents = jis_obj(root) ? jget(root, "contents") : NULL;
if (!contents || contents->type != J_ARR) {
ctx_fail(ctx, "Gemini transcript must be an object with a \"contents\" array");
goto done;
}
if (jget(root, "systemInstruction")) {
char *sys = gemini_text(ctx, jget(root, "systemInstruction"), "systemInstruction");
if (!sys) goto done;
ml_push(out, msg_new("system", sys));
}
for (size_t i = 0; i < contents->n_items; i++) {
JValue *entry = contents->items[i];
char where[64];
snprintf(where, sizeof where, "contents[%zu]", i);
if (!jis_obj(entry)) {
ctx_fail(ctx, "contents[%zu] is not an object", i);
goto done;
}
const char *role = jstr(entry, "role");
if (!role || (strcmp(role, "user") != 0 && strcmp(role, "model") != 0)) {
JValue *rv = jget(entry, "role");
char *rendered = rv ? jto_compact(rv) : NULL;
ctx_fail(ctx, "%s has unsupported role: %s (Gemini uses \"user\" or \"model\")",
where, rendered ? rendered : "null");
free(rendered);
goto done;
}
JValue *parts = jget(entry, "parts");
if (!parts || parts->type != J_ARR) {
ctx_fail(ctx, "%s: parts must be an array", where);
goto done;
}
if (strcmp(role, "model") == 0) {
/* A model turn keeps its text and function calls in ONE message,
* mirroring an OpenAI assistant message with tool_calls. */
StrList texts;
sl_init(&texts);
ToolCall *calls = NULL;
size_t n_calls = 0;
for (size_t k = 0; k < parts->n_items; k++) {
JValue *part = parts->items[k];
const char *t = jstr(part, "text");
if (t) {
sl_push(&texts, xstrdup(t));
continue;
}
JValue *fc = jget(part, "functionCall");
if (fc && fc->type == J_OBJ) {
const char *name = jstr(fc, "name");
calls = xrealloc(calls, (n_calls + 1) * sizeof *calls);
calls[n_calls].id = xstrdup("");
calls[n_calls].name = xstrdup(name ? name : "");
calls[n_calls].args = args_of(jget(fc, "args"));
n_calls++;
continue;
}
ctx_warn(ctx, "%s: dropped unsupported part (inlineData or similar)", where);
}
char *joined = sl_join(&texts, "\n");
Message m = msg_new("assistant", joined);
sl_free(&texts);
if (n_calls > 0) {
m.tool_calls = calls;
m.n_tool_calls = n_calls;
m.has_tool_calls = 1;
} else {
free(calls);
}
ml_push(out, m);
continue;
}
StrList texts;
sl_init(&texts);
for (size_t k = 0; k < parts->n_items; k++) {
JValue *part = parts->items[k];
const char *t = jstr(part, "text");
if (t) {
sl_push(&texts, xstrdup(t));
continue;
}
JValue *fc = jget(part, "functionCall");
if (fc && fc->type == J_OBJ) {
flush_user_texts(out, &texts);
Message m = msg_new("assistant", xstrdup(""));
const char *name = jstr(fc, "name");
m.tool_calls = xmalloc(sizeof *m.tool_calls);
m.tool_calls[0].id = xstrdup("");
m.tool_calls[0].name = xstrdup(name ? name : "");
m.tool_calls[0].args = args_of(jget(fc, "args"));
m.n_tool_calls = 1;
m.has_tool_calls = 1;
ml_push(out, m);
continue;
}
JValue *fr = jget(part, "functionResponse");
if (fr && fr->type == J_OBJ) {
flush_user_texts(out, &texts);
const char *name = jstr(fr, "name");
Message m = msg_new("tool", args_of(jget(fr, "response")));
m.name = xstrdup(name ? name : "");
m.tool_call_id = xstrdup(name ? name : "");
ml_push(out, m);
continue;
}
ctx_warn(ctx, "%s: dropped unsupported part (inlineData or similar)", where);
}
flush_user_texts(out, &texts);
sl_free(&texts);
}
rc = 0;
done:
jfree(root);
return rc;
}
/* A parsed Markdown header line: `**role**: text`. Returns 0 on no match. */
static int markdown_header(const char *line, char role[16], char **first) {
if (strncmp(line, "**", 2) != 0) return 0;
const char *rest = line + 2;
const char *end = strstr(rest, "**:");
if (!end) return 0;
size_t role_len = (size_t) (end - rest);
const char *mapped = NULL;
if (role_len == 6 && strncmp(rest, "system", 6) == 0) mapped = "system";
else if (role_len == 4 && strncmp(rest, "user", 4) == 0) mapped = "user";
else if (role_len == 9 && strncmp(rest, "assistant", 9) == 0) mapped = "assistant";
else if (role_len == 5 && strncmp(rest, "model", 5) == 0) mapped = "assistant";
else if (role_len == 4 && strncmp(rest, "tool", 4) == 0) mapped = "tool";
if (!mapped) return 0;
snprintf(role, 16, "%s", mapped);
const char *p = end + 3;
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == '\x0b' || *p == '\x0c') p++;
*first = xstrdup(p);
return 1;
}
static Message finish_markdown_message(const char *role, StrList *lines) {
/* Trim leading/trailing blank continuation lines but keep inner blank lines. */
while (lines->n > 1 && is_blank(lines->items[0])) {
free(lines->items[0]);
memmove(lines->items, lines->items + 1, (lines->n - 1) * sizeof *lines->items);
lines->n--;
}
while (lines->n > 1 && is_blank(lines->items[lines->n - 1])) {
free(lines->items[lines->n - 1]);
lines->n--;
}
char *joined = sl_join(lines, "\n");
return msg_new(role, joined);
}
/* Markdown transcript: `**role**: text` header lines with continuation lines
* belonging to the same message. `model` maps to assistant. */
static int parse_markdown(Ctx *ctx, const char *text, MsgList *out) {
StrList lines;
sl_init(&lines);
char role[16] = "";
int in_message = 0;
size_t start = 0;
for (;;) {
const char *nl = strchr(text + start, '\n');
size_t len = nl ? (size_t) (nl - (text + start)) : strlen(text + start);
char *line = xstrndup(text + start, len);
char hdr_role[16], *hdr_first = NULL;
if (markdown_header(line, hdr_role, &hdr_first)) {
if (in_message) {
ml_push(out, finish_markdown_message(role, &lines));
sl_free(&lines);
}
snprintf(role, sizeof role, "%s", hdr_role);
sl_init(&lines);
sl_push(&lines, hdr_first);
in_message = 1;
} else if (in_message) {
sl_push(&lines, line);
} else if (!is_blank(line)) {
free(line);
sl_free(&lines);
ctx_fail(ctx, "Markdown transcript must start with a `**role**:` header line");
return -1;
} else {
free(line);
}
if (!nl) break;
start = (size_t) (nl - text) + 1;
}
if (in_message) ml_push(out, finish_markdown_message(role, &lines));
sl_free(&lines);
return 0;
}
/* -------------------------------- serialize ------------------------------- */
static char *serialize_openai(MsgList *msgs, Ctx *ctx) {
(void) ctx;
JValue *root = jnew(J_OBJ);
JValue *arr = jnew(J_ARR);
for (size_t i = 0; i < msgs->n; i++) {
Message *m = &msgs->items[i];
JValue *o = jnew(J_OBJ);
if (strcmp(m->role, "tool") == 0) {
jobj_put(o, "role", jstr_new("tool"));
jobj_put(o, "content", jstr_new(m->content));
jobj_put(o, "tool_call_id", jstr_new(m->tool_call_id ? m->tool_call_id : ""));
if (m->name) jobj_put(o, "name", jstr_new(m->name));
} else if (strcmp(m->role, "assistant") == 0 && m->has_tool_calls) {
jobj_put(o, "role", jstr_new("assistant"));
if (m->content[0] == '\0') jobj_put(o, "content", jnew(J_NULL));
else jobj_put(o, "content", jstr_new(m->content));
JValue *calls = jnew(J_ARR);
for (size_t k = 0; k < m->n_tool_calls; k++) {
ToolCall *tc = &m->tool_calls[k];
JValue *call = jnew(J_OBJ);
jobj_put(call, "id", jstr_new(tc->id));
jobj_put(call, "type", jstr_new("function"));
JValue *fn = jnew(J_OBJ);
jobj_put(fn, "name", jstr_new(tc->name));
jobj_put(fn, "arguments", jstr_new(tc->args));
jobj_put(call, "function", fn);
jarr_push(calls, call);
}
jobj_put(o, "tool_calls", calls);
if (m->name) jobj_put(o, "name", jstr_new(m->name));
} else {
jobj_put(o, "role", jstr_new(m->role));
jobj_put(o, "content", jstr_new(m->content));
if (m->name) jobj_put(o, "name", jstr_new(m->name));
}
jarr_push(arr, o);
}
jobj_put(root, "messages", arr);
StrBuf out;
sb_init(&out);
char *pretty = jto_pretty(root);
sb_append(&out, pretty);
sb_append_n(&out, "\n", 1);
free(pretty);
jfree(root);
return out.data;
}
static char *serialize_anthropic(MsgList *msgs, Ctx *ctx) {
StrList system;
sl_init(&system);
JValue *arr = jnew(J_ARR);
for (size_t i = 0; i < msgs->n; i++) {
Message *m = &msgs->items[i];
if (strcmp(m->role, "system") == 0) {
sl_push(&system, xstrdup(m->content));
continue;
}
if (m->name && strcmp(m->role, "tool") != 0) {
ctx_warn(ctx, "message %zu: \"name\" has no Anthropic equivalent and was dropped", i);
}
JValue *o = jnew(J_OBJ);
if (strcmp(m->role, "user") == 0) {
jobj_put(o, "role", jstr_new("user"));
jobj_put(o, "content", jstr_new(m->content));
} else if (strcmp(m->role, "assistant") == 0) {
jobj_put(o, "role", jstr_new("assistant"));
if (m->has_tool_calls) {
JValue *blocks = jnew(J_ARR);
if (m->content[0] != '\0') {
JValue *tb = jnew(J_OBJ);
jobj_put(tb, "type", jstr_new("text"));
jobj_put(tb, "text", jstr_new(m->content));
jarr_push(blocks, tb);
}
for (size_t k = 0; k < m->n_tool_calls; k++) {
ToolCall *tc = &m->tool_calls[k];
JValue *ub = jnew(J_OBJ);
jobj_put(ub, "type", jstr_new("tool_use"));
jobj_put(ub, "id", jstr_new(tc->id));
jobj_put(ub, "name", jstr_new(tc->name));
jobj_put(ub, "input", parse_args_or_empty(tc->args));
jarr_push(blocks, ub);
}
jobj_put(o, "content", blocks);
} else {
jobj_put(o, "content", jstr_new(m->content));
}
} else {
jobj_put(o, "role", jstr_new("user"));
JValue *rb = jnew(J_OBJ);
jobj_put(rb, "type", jstr_new("tool_result"));
jobj_put(rb, "tool_use_id", jstr_new(m->tool_call_id ? m->tool_call_id : ""));
jobj_put(rb, "content", jstr_new(m->content));
JValue *wrapped = jnew(J_ARR);
jarr_push(wrapped, rb);
jobj_put(o, "content", wrapped);
}
jarr_push(arr, o);
}
JValue *root = jnew(J_OBJ);
if (arr->n_items > 0) jobj_put(root, "messages", arr);
else jfree(arr);
if (system.n > 0) {
char *joined = sl_join(&system, "\n\n");
jobj_put(root, "system", jstr_new(joined));
free(joined);
}
sl_free(&system);
StrBuf out;
sb_init(&out);
char *pretty = jto_pretty(root);
sb_append(&out, pretty);
sb_append_n(&out, "\n", 1);
free(pretty);
jfree(root);
return out.data;
}
/* (role, parts) turns; consecutive same-role parts merge into one turn. */
typedef struct {
const char *role;
JValue *parts; /* J_ARR */
} Turn;
static void push_turn(Turn **turns, size_t *n, size_t *cap, const char *role, JValue *part) {
if (*n > 0 && strcmp((*turns)[*n - 1].role, role) == 0) {
jarr_push((*turns)[*n - 1].parts, part);
return;
}
if (*n == *cap) {
*cap = *cap ? *cap * 2 : 8;
*turns = xrealloc(*turns, *cap * sizeof **turns);
}
JValue *parts = jnew(J_ARR);
jarr_push(parts, part);
(*turns)[*n].role = role;
(*turns)[*n].parts = parts;
(*n)++;
}
static char *serialize_gemini(MsgList *msgs, Ctx *ctx) {
StrList system;
sl_init(&system);
Turn *turns = NULL;
size_t n_turns = 0, cap_turns = 0;
for (size_t i = 0; i < msgs->n; i++) {
Message *m = &msgs->items[i];
if (strcmp(m->role, "system") == 0) {
sl_push(&system, xstrdup(m->content));
continue;
}
if (m->name && strcmp(m->role, "tool") != 0) {
ctx_warn(ctx, "message %zu: \"name\" has no Gemini equivalent and was dropped", i);
}
if (strcmp(m->role, "user") == 0) {
JValue *p = jnew(J_OBJ);
jobj_put(p, "text", jstr_new(m->content));
push_turn(&turns, &n_turns, &cap_turns, "user", p);
} else if (strcmp(m->role, "assistant") == 0) {
if (m->content[0] != '\0') {
JValue *p = jnew(J_OBJ);
jobj_put(p, "text", jstr_new(m->content));
push_turn(&turns, &n_turns, &cap_turns, "model", p);
}
for (size_t k = 0; k < m->n_tool_calls; k++) {
ToolCall *tc = &m->tool_calls[k];
JValue *fc = jnew(J_OBJ);
jobj_put(fc, "name", jstr_new(tc->name));
jobj_put(fc, "args", parse_args_or_empty(tc->args));
JValue *p = jnew(J_OBJ);
jobj_put(p, "functionCall", fc);
push_turn(&turns, &n_turns, &cap_turns, "model", p);
}
} else {
const char *name = m->name ? m->name : (m->tool_call_id ? m->tool_call_id : "");
JValue *fr = jnew(J_OBJ);
jobj_put(fr, "name", jstr_new(name));
jobj_put(fr, "response", parse_args_or_empty(m->content));
JValue *p = jnew(J_OBJ);
jobj_put(p, "functionResponse", fr);
push_turn(&turns, &n_turns, &cap_turns, "user", p);
}
}
JValue *root = jnew(J_OBJ);
if (n_turns > 0) {
JValue *contents = jnew(J_ARR);
for (size_t i = 0; i < n_turns; i++) {
JValue *c = jnew(J_OBJ);
jobj_put(c, "role", jstr_new(turns[i].role));
jobj_put(c, "parts", turns[i].parts);
jarr_push(contents, c);
}
jobj_put(root, "contents", contents);
}
if (system.n > 0) {
char *joined = sl_join(&system, "\n\n");
JValue *tp = jnew(J_OBJ);
jobj_put(tp, "text", jstr_new(joined));
free(joined);
JValue *one = jnew(J_ARR);
jarr_push(one, tp);
JValue *si = jnew(J_OBJ);
jobj_put(si, "parts", one);
jobj_put(root, "systemInstruction", si);
}
sl_free(&system);
free(turns);
StrBuf out;
sb_init(&out);
char *pretty = jto_pretty(root);
sb_append(&out, pretty);
sb_append_n(&out, "\n", 1);
free(pretty);
jfree(root);
return out.data;
}
static char *serialize_markdown(MsgList *msgs, Ctx *ctx) {
StrBuf out;
sb_init(&out);
for (size_t i = 0; i < msgs->n; i++) {
Message *m = &msgs->items[i];
if (m->n_tool_calls > 0) {
ctx_warn(ctx, "message %zu: tool calls are not representable in Markdown and were dropped", i);
}
if (strcmp(m->role, "tool") == 0 && m->tool_call_id) {
ctx_warn(ctx, "message %zu: tool result id is not representable in Markdown and was dropped", i);
}
if (i > 0) sb_append_n(&out, "\n\n", 2);
sb_appendf(&out, "**%s**: %s", m->role, m->content);
}
sb_append_n(&out, "\n", 1);
return out.data;
}
/* --------------------------------- convert -------------------------------- */
typedef int (*ParserFn)(Ctx *, const char *, MsgList *);
typedef char *(*SerializerFn)(MsgList *, Ctx *);
/* Convert a transcript between chat formats through the shared internal shape.
*
* Returns a malloc'd ConversionResult, or NULL with the message in `err`
* (errsz bytes) when the transcript is empty, the source format fails to
* parse, or from/to is not a known format. Free with conversion_result_free. */
static ConversionResult *convert(const char *transcript, const char *from, const char *to,
char *err, size_t errsz) {
char *trimmed = trim_dup(transcript);
int empty = trimmed[0] == '\0';
free(trimmed);
if (empty) {
snprintf(err, errsz, "Transcript is empty — paste a transcript first");
return NULL;
}
ParserFn parser = strcmp(from, "openai") == 0 ? parse_openai
: strcmp(from, "anthropic") == 0 ? parse_anthropic
: strcmp(from, "gemini") == 0 ? parse_gemini
: strcmp(from, "markdown") == 0 ? parse_markdown : NULL;
if (!parser) {
snprintf(err, errsz, "Unknown source format: %s", from);
return NULL;
}
SerializerFn serializer = strcmp(to, "openai") == 0 ? serialize_openai
: strcmp(to, "anthropic") == 0 ? serialize_anthropic
: strcmp(to, "gemini") == 0 ? serialize_gemini
: strcmp(to, "markdown") == 0 ? serialize_markdown : NULL;
if (!serializer) {
snprintf(err, errsz, "Unknown target format: %s", to);
return NULL;
}
Ctx ctx;
memset(&ctx, 0, sizeof ctx);
MsgList msgs;
ml_init(&msgs);
if (parser(&ctx, transcript, &msgs) != 0) {
snprintf(err, errsz, "%s", ctx.err);
ml_free(&msgs);
for (size_t i = 0; i < ctx.n_warnings; i++) free(ctx.warnings[i]);
free(ctx.warnings);
return NULL;
}
char *output = serializer(&msgs, &ctx);
ml_free(&msgs);
ConversionResult *r = xmalloc(sizeof *r);
r->output = output;
r->warnings = ctx.warnings;
r->n_warnings = ctx.n_warnings;
return r;
}
static void conversion_result_free(ConversionResult *r) {
if (!r) return;
free(r->output);
for (size_t i = 0; i < r->n_warnings; i++) free(r->warnings[i]);
free(r->warnings);
free(r);
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →