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# (12, standard library only — System.Text.Json)
// 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.
// System.Text.Json enumerates and writes object properties in document order,
// so key order survives a round-trip exactly like the TypeScript original.
// Parse failures throw ArgumentException.
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace CosmoDev.ChatFormatConverter;
public static class ChatFormats
{
public static readonly string[] All = ["openai", "anthropic", "gemini", "markdown"];
}
/// One assistant-emitted tool call, provider-agnostic. <c>Args</c> is a JSON string.
public sealed record ToolCall(string Id = "", string Name = "", string Args = "");
/// The shared internal shape every format parses into and serializes from.
public sealed record InternalMessage(
string Role, // "system" | "user" | "assistant" | "tool"
string Content = "", // "" when the message only carries tool calls/results
string? Name = null,
IReadOnlyList<ToolCall>? ToolCalls = null,
string? ToolCallId = null);
/// The conversion output plus human-readable notes about unmapped fields.
public sealed record ConversionResult(string Output, IReadOnlyList<string> Warnings);
public static partial class ChatFormatConverter
{
/* -------------------------------- helpers -------------------------------- */
private static JsonDocument ParseTranscriptJson(string text, string label)
{
try
{
return JsonDocument.Parse(text);
}
catch (JsonException e)
{
throw new ArgumentException($"{label}: invalid JSON — {e.Message}");
}
}
private static string Str(JsonElement el, string key) =>
el.ValueKind == JsonValueKind.Object
&& el.TryGetProperty(key, out var v)
&& v.ValueKind == JsonValueKind.String ? v.GetString()! : null
?? "";
/// Like <see cref="Str"/> but keeps the string/missing distinction: a JSON
/// string value (even "") is present, anything else is null.
private static string? StrOrNull(JsonElement el, string key) =>
el.ValueKind == JsonValueKind.Object
&& el.TryGetProperty(key, out var v)
&& v.ValueKind == JsonValueKind.String ? v.GetString()! : null;
private static bool Has(JsonElement el, string key) =>
el.ValueKind == JsonValueKind.Object && el.TryGetProperty(key, out _);
private static JsonElement? Get(JsonElement el, string key) =>
el.ValueKind == JsonValueKind.Object && el.TryGetProperty(key, out var v) ? v : null;
/// The TS <c>?? {}</c> capture: null/missing becomes {}, any other value
/// passes through and stringifies as-is.
private static string ArgsOf(JsonElement? v)
{
if (v is null || v.Value.ValueKind == JsonValueKind.Null) return "{}";
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms)) v.Value.WriteTo(w);
return Encoding.UTF8.GetString(ms.ToArray());
}
/// Parse a JSON-string argument back to an element, an empty object when
/// it is not a JSON object (mirrors parseArgsOrEmpty).
private static JsonElement ParseArgsOrEmpty(string args)
{
try
{
var doc = JsonDocument.Parse(args);
if (doc.RootElement.ValueKind == JsonValueKind.Object) return doc.RootElement;
doc.Dispose();
}
catch (JsonException) { }
return JsonDocument.Parse("{}").RootElement;
}
private static string BlockTypeName(JsonElement raw) =>
raw.ValueKind == JsonValueKind.Object
&& raw.TryGetProperty("type", out var t)
&& t.ValueKind == JsonValueKind.String ? t.GetString()! : "content";
private static string Compact(JsonElement el)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms)) el.WriteTo(w);
return Encoding.UTF8.GetString(ms.ToArray());
}
/* ---------------------------------- parse -------------------------------- */
/// OpenAI messages[] (bare array, or wrapped in {"messages": [...]}).
public static List<InternalMessage> ParseOpenAi(string text, List<string> warnings)
{
using var doc = ParseTranscriptJson(text, "OpenAI transcript");
var root = doc.RootElement;
JsonElement items = root.ValueKind == JsonValueKind.Array ? root
: Get(root, "messages") is { ValueKind: JsonValueKind.Array } messages ? messages
: throw new ArgumentException(
"""OpenAI transcript must be a messages[] array or an object with a "messages" array""");
var @out = new List<InternalMessage>();
var i = 0;
foreach (var entry in items.EnumerateArray())
{
if (entry.ValueKind != JsonValueKind.Object)
throw new ArgumentException($"messages[{i}] is not an object");
var where = $"messages[{i}]";
var role = Str(entry, "role");
switch (role)
{
case "system" or "developer":
@out.Add(new("system", ContentText(Get(entry, "content"), where, warnings)));
break;
case "user" or "assistant" or "tool":
var msg = new InternalMessage(role, ContentText(Get(entry, "content"), where, warnings))
{
Name = StrOrNull(entry, "name"),
ToolCallId = StrOrNull(entry, "tool_call_id"),
ToolCalls = ParseOpenAiToolCalls(Get(entry, "tool_calls")),
};
@out.Add(msg);
break;
default:
throw new ArgumentException(
$"messages[{i}] has unsupported role: {(Has(entry, "role") ? Compact(entry.GetProperty("role")) : "null")}");
}
i++;
}
return @out;
}
/// OpenAI content → plain text (string, or text parts joined; other parts flagged).
private static string ContentText(JsonElement? content, string where, List<string> warnings)
{
switch (content)
{
case null or { ValueKind: JsonValueKind.Null }:
return "";
case { ValueKind: JsonValueKind.String } s:
return s.GetString()!;
case { ValueKind: JsonValueKind.Array } parts:
{
var texts = new List<string>();
var dropped = 0;
foreach (var part in parts.EnumerateArray())
{
var isText = Str(part, "type") == "text"
&& Get(part, "text") is { ValueKind: JsonValueKind.String };
if (isText) texts.Add(part.GetProperty("text").GetString()!);
else dropped++;
}
if (dropped > 0)
warnings.Add($"{where}: dropped {dropped} non-text content part(s)");
return string.Concat(texts);
}
default:
throw new ArgumentException($"{where}: content must be a string, an array of parts, or null");
}
}
private static IReadOnlyList<ToolCall>? ParseOpenAiToolCalls(JsonElement? raw)
{
if (raw is null or { ValueKind: JsonValueKind.Null }) return null;
if (raw.Value.ValueKind != JsonValueKind.Array)
throw new ArgumentException("tool_calls must be an array");
var calls = new List<ToolCall>();
foreach (var entry in raw.Value.EnumerateArray())
{
var fn = Get(entry, "function");
calls.Add(new ToolCall(
Id: Str(entry, "id"),
Name: fn is { } f ? Str(f, "name") : "",
Args: fn is { } f2 ? Str(f2, "arguments") : ""));
}
return calls;
}
/// One normalized Anthropic content block.
private enum BlockKind { Text, ToolUse, ToolResult, Other }
private static List<(BlockKind Kind, string Text, JsonElement Raw)> ContentBlocks(
JsonElement? content, string where)
{
if (content is { ValueKind: JsonValueKind.String } s)
return [(BlockKind.Text, s.GetString()!, default)];
if (content is not { ValueKind: JsonValueKind.Array } blocks)
throw new ArgumentException($"{where}: content must be a string or an array of blocks");
var @out = new List<(BlockKind, string, JsonElement)>();
foreach (var block in blocks.EnumerateArray())
{
var isText = Str(block, "type") == "text"
&& Get(block, "text") is { ValueKind: JsonValueKind.String };
var kind = Str(block, "type");
if (isText) @out.Add((BlockKind.Text, block.GetProperty("text").GetString()!, default));
else if (kind == "tool_use") @out.Add((BlockKind.ToolUse, "", block));
else if (kind == "tool_result") @out.Add((BlockKind.ToolResult, "", block));
else @out.Add((BlockKind.Other, "", block));
}
return @out;
}
/// Anthropic string-or-block[] content → plain text.
private static string BlocksToText(JsonElement? content, string where, List<string> warnings)
{
if (content is null or { ValueKind: JsonValueKind.Null }) return "";
if (content is { ValueKind: JsonValueKind.String } s) return s.GetString()!;
var texts = new List<string>();
foreach (var (kind, text, raw) in ContentBlocks(content, where))
{
switch (kind)
{
case BlockKind.Text:
texts.Add(text);
break;
case BlockKind.Other:
warnings.Add($"{where}: dropped unsupported {BlockTypeName(raw)} block");
break;
default:
warnings.Add($"{where}: dropped {kind.ToString().ToLowerInvariant()} block from text-only content");
break;
}
}
return string.Join("\n", texts);
}
private static ToolCall ToolCallFromBlock(JsonElement raw) => new(
Id: Str(raw, "id"),
Name: Str(raw, "name"),
Args: ArgsOf(Get(raw, "input")));
/// Anthropic messages[] plus the top-level system field.
public static List<InternalMessage> ParseAnthropic(string text, List<string> warnings)
{
using var doc = ParseTranscriptJson(text, "Anthropic transcript");
var root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Object
|| Get(root, "messages") is not { ValueKind: JsonValueKind.Array })
throw new ArgumentException("""Anthropic transcript must be an object with a "messages" array""");
var @out = new List<InternalMessage>();
if (Has(root, "system"))
@out.Add(new("system", BlocksToText(Get(root, "system"), "system", warnings)));
var i = 0;
foreach (var entry in root.GetProperty("messages").EnumerateArray())
{
if (entry.ValueKind != JsonValueKind.Object)
throw new ArgumentException($"messages[{i}] is not an object");
var where = $"messages[{i}]";
switch (Str(entry, "role"))
{
case "assistant":
{
var texts = new List<string>();
var calls = new List<ToolCall>();
foreach (var (kind, t, raw) in ContentBlocks(Get(entry, "content"), where))
{
switch (kind)
{
case BlockKind.Text: texts.Add(t); break;
case BlockKind.ToolUse: calls.Add(ToolCallFromBlock(raw)); break;
case BlockKind.Other:
warnings.Add($"{where}: dropped unsupported {BlockTypeName(raw)} block");
break;
}
}
var msg = new InternalMessage("assistant", string.Join("\n", texts));
if (calls.Count > 0) msg = msg with { ToolCalls = calls };
@out.Add(msg);
break;
}
case "user":
{
var texts = new List<string>();
void Flush()
{
if (texts.Count > 0)
{
@out.Add(new("user", string.Join("\n", texts)));
texts.Clear();
}
}
foreach (var (kind, t, raw) in ContentBlocks(Get(entry, "content"), where))
{
switch (kind)
{
case BlockKind.Text:
texts.Add(t);
break;
case BlockKind.ToolUse:
warnings.Add($"{where}: tool_use block inside a user message moved to an assistant tool call");
Flush();
@out.Add(new("assistant", "", ToolCalls: [ToolCallFromBlock(raw)]));
break;
case BlockKind.ToolResult:
Flush();
@out.Add(new("tool",
BlocksToText(Get(raw, "content"), where, warnings),
ToolCallId: Str(raw, "tool_use_id")));
break;
default:
warnings.Add($"{where}: dropped unsupported {BlockTypeName(raw)} block");
break;
}
}
Flush();
break;
}
default:
throw new ArgumentException(
$"{where} has unsupported role: {(Has(entry, "role") ? Compact(entry.GetProperty("role")) : "null")}");
}
i++;
}
return @out;
}
/// Gemini systemInstruction (string or {parts}) → plain text.
private static string GeminiText(JsonElement v, string where, List<string> warnings)
{
if (v.ValueKind == JsonValueKind.String) return v.GetString()!;
if (v.ValueKind == JsonValueKind.Object)
{
if (Get(v, "text") is { ValueKind: JsonValueKind.String } t) return t.GetString()!;
if (Get(v, "parts") is { ValueKind: JsonValueKind.Array } parts)
return string.Join("\n", parts.EnumerateArray().Select(p => Str(p, "text")));
}
warnings.Add($"{where}: unsupported systemInstruction shape, treated as empty");
return "";
}
/// Gemini contents[] plus the optional systemInstruction.
public static List<InternalMessage> ParseGemini(string text, List<string> warnings)
{
using var doc = ParseTranscriptJson(text, "Gemini transcript");
var root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Object
|| Get(root, "contents") is not { ValueKind: JsonValueKind.Array })
throw new ArgumentException("""Gemini transcript must be an object with a "contents" array""");
var @out = new List<InternalMessage>();
if (Has(root, "systemInstruction"))
@out.Add(new("system", GeminiText(root.GetProperty("systemInstruction"), "systemInstruction", warnings)));
var i = 0;
foreach (var entry in root.GetProperty("contents").EnumerateArray())
{
if (entry.ValueKind != JsonValueKind.Object)
throw new ArgumentException($"contents[{i}] is not an object");
var where = $"contents[{i}]";
var role = Str(entry, "role");
if (role is not ("user" or "model"))
throw new ArgumentException(
$"{where} has unsupported role: {(Has(entry, "role") ? Compact(entry.GetProperty("role")) : "null")} (Gemini uses \"user\" or \"model\")");
var parts = Get(entry, "parts");
if (parts is not { ValueKind: JsonValueKind.Array })
throw new ArgumentException($"{where}: parts must be an array");
if (role == "model")
{
// A model turn keeps its text and function calls in ONE message,
// mirroring an OpenAI assistant message with tool_calls.
var texts = new List<string>();
var calls = new List<ToolCall>();
foreach (var part in parts.Value.EnumerateArray())
{
if (Get(part, "text") is { ValueKind: JsonValueKind.String } t)
{
texts.Add(t.GetString()!);
continue;
}
if (Get(part, "functionCall") is { ValueKind: JsonValueKind.Object } fc)
{
calls.Add(new ToolCall(Name: Str(fc, "name"), Args: ArgsOf(Get(fc, "args"))));
continue;
}
warnings.Add($"{where}: dropped unsupported part (inlineData or similar)");
}
var msg = new InternalMessage("assistant", string.Join("\n", texts));
if (calls.Count > 0) msg = msg with { ToolCalls = calls };
@out.Add(msg);
i++;
continue;
}
var userTexts = new List<string>();
void FlushText()
{
if (userTexts.Count > 0)
{
@out.Add(new("user", string.Join("\n", userTexts)));
userTexts.Clear();
}
}
foreach (var part in parts.Value.EnumerateArray())
{
if (Get(part, "text") is { ValueKind: JsonValueKind.String } t)
{
userTexts.Add(t.GetString()!);
continue;
}
if (Get(part, "functionCall") is { ValueKind: JsonValueKind.Object } fc)
{
FlushText();
@out.Add(new("assistant", "",
ToolCalls: [new ToolCall(Name: Str(fc, "name"), Args: ArgsOf(Get(fc, "args")))]));
continue;
}
if (Get(part, "functionResponse") is { ValueKind: JsonValueKind.Object } fr)
{
FlushText();
var name = Str(fr, "name");
@out.Add(new("tool", ArgsOf(Get(fr, "response")), name, null, name));
continue;
}
warnings.Add($"{where}: dropped unsupported part (inlineData or similar)");
}
FlushText();
i++;
}
return @out;
}
[GeneratedRegex(@"^\*\*(system|user|assistant|model|tool)\*\*:\s*(.*)$")]
private static partial Regex MarkdownHeaderRegex();
private static InternalMessage FinishMessage(string role, List<string> lines)
{
// Trim leading/trailing blank continuation lines but keep inner blank lines.
while (lines.Count > 1 && lines[0].Trim().Length == 0) lines.RemoveAt(0);
while (lines.Count > 1 && lines[^1].Trim().Length == 0) lines.RemoveAt(lines.Count - 1);
return new(role, string.Join("\n", lines));
}
/// Markdown transcript: `**role**: text` header lines with continuation
/// lines belonging to the same message. `model` maps to assistant.
public static List<InternalMessage> ParseMarkdown(string text, List<string> _)
{
var @out = new List<InternalMessage>();
(string Role, List<string> Lines)? current = null;
foreach (var line in text.Split('\n'))
{
var match = MarkdownHeaderRegex().Match(line);
if (match.Success)
{
if (current is { } c) @out.Add(FinishMessage(c.Role, c.Lines));
current = (match.Groups[1].Value == "model" ? "assistant" : match.Groups[1].Value,
[match.Groups[2].Value]);
continue;
}
if (current is { } cur) cur.Lines.Add(line);
else if (line.Trim().Length != 0)
throw new ArgumentException("Markdown transcript must start with a `**role**:` header line");
}
if (current is { } last) @out.Add(FinishMessage(last.Role, last.Lines));
return @out;
}
/* -------------------------------- serialize ------------------------------ */
private static string WritePretty(Action<Utf8JsonWriter> write)
{
using var ms = new MemoryStream();
using (var w = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true })) write(w);
return Encoding.UTF8.GetString(ms.ToArray()) + "\n";
}
public static string SerializeOpenAi(List<InternalMessage> messages, List<string> _)
{
return WritePretty(w =>
{
w.WriteStartObject();
w.WritePropertyName("messages");
w.WriteStartArray();
foreach (var m in messages)
{
w.WriteStartObject();
if (m.Role == "tool")
{
w.WriteString("role", "tool");
w.WriteString("content", m.Content);
w.WriteString("tool_call_id", m.ToolCallId ?? "");
if (m.Name is { } name) w.WriteString("name", name);
}
else if (m.Role == "assistant" && m.ToolCalls is { })
{
w.WriteString("role", "assistant");
if (m.Content == "") w.WriteNull("content");
else w.WriteString("content", m.Content);
w.WritePropertyName("tool_calls");
w.WriteStartArray();
foreach (var tc in m.ToolCalls)
{
w.WriteStartObject();
w.WriteString("id", tc.Id);
w.WriteString("type", "function");
w.WritePropertyName("function");
w.WriteStartObject();
w.WriteString("name", tc.Name);
w.WriteString("arguments", tc.Args);
w.WriteEndObject();
w.WriteEndObject();
}
w.WriteEndArray();
if (m.Name is { } name) w.WriteString("name", name);
}
else
{
w.WriteString("role", m.Role);
w.WriteString("content", m.Content);
if (m.Name is { } name) w.WriteString("name", name);
}
w.WriteEndObject();
}
w.WriteEndArray();
w.WriteEndObject();
});
}
public static string SerializeAnthropic(List<InternalMessage> messages, List<string> warnings)
{
var system = messages.Where(m => m.Role == "system").Select(m => m.Content).ToList();
var wroteMessages = false;
var output = WritePretty(w =>
{
w.WriteStartObject();
var i = 0;
foreach (var m in messages)
{
var index = i++;
if (m.Role == "system") continue;
if (m.Name is not null && m.Role != "tool")
warnings.Add($"message {index}: \"name\" has no Anthropic equivalent and was dropped");
if (!wroteMessages)
{
w.WritePropertyName("messages");
w.WriteStartArray();
wroteMessages = true;
}
w.WriteStartObject();
switch (m.Role)
{
case "user":
w.WriteString("role", "user");
w.WriteString("content", m.Content);
break;
case "assistant" when m.ToolCalls is { } calls:
w.WriteString("role", "assistant");
w.WritePropertyName("content");
w.WriteStartArray();
if (m.Content != "")
{
w.WriteStartObject();
w.WriteString("type", "text");
w.WriteString("text", m.Content);
w.WriteEndObject();
}
foreach (var tc in calls)
{
w.WriteStartObject();
w.WriteString("type", "tool_use");
w.WriteString("id", tc.Id);
w.WriteString("name", tc.Name);
w.WritePropertyName("input");
ParseArgsOrEmpty(tc.Args).WriteTo(w);
w.WriteEndObject();
}
w.WriteEndArray();
break;
case "assistant":
w.WriteString("role", "assistant");
w.WriteString("content", m.Content);
break;
default:
w.WriteString("role", "user");
w.WritePropertyName("content");
w.WriteStartArray();
w.WriteStartObject();
w.WriteString("type", "tool_result");
w.WriteString("tool_use_id", m.ToolCallId ?? "");
w.WriteString("content", m.Content);
w.WriteEndObject();
w.WriteEndArray();
break;
}
w.WriteEndObject();
}
if (wroteMessages) w.WriteEndArray();
if (system.Count > 0) w.WriteString("system", string.Join("\n\n", system));
w.WriteEndObject();
});
return output;
}
public static string SerializeGemini(List<InternalMessage> messages, List<string> warnings)
{
var system = messages.Where(m => m.Role == "system").Select(m => m.Content).ToList();
// (role, parts) turns; consecutive same-role parts merge into one turn.
var turns = new List<(string Role, List<Action<Utf8JsonWriter>> Parts)>();
void Push(string role, Action<Utf8JsonWriter> part)
{
if (turns.Count > 0 && turns[^1].Role == role) turns[^1].Parts.Add(part);
else turns.Add((role, [part]));
}
var i = 0;
foreach (var m in messages)
{
var index = i++;
if (m.Role == "system") continue;
if (m.Name is not null && m.Role != "tool")
warnings.Add($"message {index}: \"name\" has no Gemini equivalent and was dropped");
switch (m.Role)
{
case "user":
Push("user", w => w.WriteString("text", m.Content));
break;
case "assistant":
if (m.Content != "") Push("model", w => w.WriteString("text", m.Content));
foreach (var tc in m.ToolCalls ?? [])
Push("model", w =>
{
w.WritePropertyName("functionCall");
w.WriteStartObject();
w.WriteString("name", tc.Name);
w.WritePropertyName("args");
ParseArgsOrEmpty(tc.Args).WriteTo(w);
w.WriteEndObject();
});
break;
default:
var name = m.Name ?? m.ToolCallId ?? "";
Push("user", w =>
{
w.WritePropertyName("functionResponse");
w.WriteStartObject();
w.WriteString("name", name);
w.WritePropertyName("response");
ParseArgsOrEmpty(m.Content).WriteTo(w);
w.WriteEndObject();
});
break;
}
}
return WritePretty(w =>
{
w.WriteStartObject();
if (turns.Count > 0)
{
w.WritePropertyName("contents");
w.WriteStartArray();
foreach (var (role, parts) in turns)
{
w.WriteStartObject();
w.WriteString("role", role);
w.WritePropertyName("parts");
w.WriteStartArray();
foreach (var part in parts)
{
w.WriteStartObject();
part(w);
w.WriteEndObject();
}
w.WriteEndArray();
w.WriteEndObject();
}
w.WriteEndArray();
}
if (system.Count > 0)
{
w.WritePropertyName("systemInstruction");
w.WriteStartObject();
w.WritePropertyName("parts");
w.WriteStartArray();
w.WriteStartObject();
w.WriteString("text", string.Join("\n\n", system));
w.WriteEndObject();
w.WriteEndArray();
w.WriteEndObject();
}
w.WriteEndObject();
});
}
public static string SerializeMarkdown(List<InternalMessage> messages, List<string> warnings)
{
for (var i = 0; i < messages.Count; i++)
{
var m = messages[i];
if (m.ToolCalls is { Count: > 0 })
warnings.Add($"message {i}: tool calls are not representable in Markdown and were dropped");
if (m.Role == "tool" && m.ToolCallId is not null)
warnings.Add($"message {i}: tool result id is not representable in Markdown and was dropped");
}
return string.Join("\n\n", messages.Select(m => $"**{m.Role}**: {m.Content}")) + "\n";
}
private static readonly Dictionary<string, Func<string, List<string>, List<InternalMessage>>> Parsers = new()
{
["openai"] = ParseOpenAi,
["anthropic"] = ParseAnthropic,
["gemini"] = ParseGemini,
["markdown"] = ParseMarkdown,
};
private static readonly Dictionary<string, Func<List<InternalMessage>, List<string>, string>> Serializers = new()
{
["openai"] = SerializeOpenAi,
["anthropic"] = SerializeAnthropic,
["gemini"] = SerializeGemini,
["markdown"] = SerializeMarkdown,
};
/// Convert a transcript between chat formats through the shared internal shape.
///
/// Throws ArgumentException when the transcript is empty, the source format
/// fails to parse, or from/to is not a known format.
public static ConversionResult Convert(string transcript, string from, string to)
{
if (string.IsNullOrWhiteSpace(transcript))
throw new ArgumentException("Transcript is empty — paste a transcript first");
if (!Parsers.TryGetValue(from, out var parser))
throw new ArgumentException($"Unknown source format: {from}");
if (!Serializers.TryGetValue(to, out var serializer))
throw new ArgumentException($"Unknown target format: {to}");
var warnings = new List<string>();
var messages = parser(transcript, warnings);
return new ConversionResult(serializer(messages, warnings), warnings);
}
}
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 →