Chat Format Converter — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* chat-format-converter — PHP 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
*
* 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.
*
* This is the PHP sibling of src/lib/chatFormatConverter.ts (the canonical
* TypeScript that powers the live tool). Standard library only; parse failures
* throw InvalidArgumentException. JSON objects decode as stdClass and arrays as
* lists, so the two stay distinguishable.
*
* Display source — part of CosmoDev's polyglot tool pages (dev.cosmolabs.org).
*/
declare(strict_types=1);
const CHAT_FORMAT_OPENAI = 'openai';
const CHAT_FORMAT_ANTHROPIC = 'anthropic';
const CHAT_FORMAT_GEMINI = 'gemini';
const CHAT_FORMAT_MARKDOWN = 'markdown';
/** One assistant-emitted tool call, provider-agnostic. `args` is a JSON string. */
final class ToolCall
{
public function __construct(
public readonly string $id = '',
public readonly string $name = '',
public readonly string $args = '',
) {
}
}
/** The shared internal shape every format parses into and serializes from. */
final class InternalMessage
{
public function __construct(
public string $role, // 'system' | 'user' | 'assistant' | 'tool'
public string $content = '',
public ?string $name = null,
/** @var list<ToolCall>|null */
public ?array $toolCalls = null,
public ?string $toolCallId = null,
) {
}
}
final class ConversionResult
{
/** @param list<string> $warnings human-readable notes about unmapped fields */
public function __construct(
public readonly string $output,
public readonly array $warnings = [],
) {
}
}
/** Parse a JSON-string argument back to a value, `{}` when it is not a JSON object. */
function cfcParseArgsOrEmpty(string $args): mixed
{
try {
$v = json_decode($args, false, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return new stdClass();
}
return $v instanceof stdClass ? $v : new stdClass();
}
/** Substitute `{}` for null/missing (mirrors TS `?? {}`); other values pass through. */
function cfcOrEmptyObject(mixed $v): mixed
{
return $v === null ? new stdClass() : $v;
}
function cfcAsRecord(mixed $v): ?stdClass
{
return $v instanceof stdClass ? $v : null;
}
/** @return list<mixed> */
function cfcAsArray(mixed $v): ?array
{
return is_array($v) ? $v : null;
}
/** Compact JSON encoding, mirroring TS JSON.stringify for captured args. */
function cfcStringify(mixed $v): string
{
return json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
/** Pretty JSON encoding for serialized transcripts — a 2-space writer matching
* JSON.stringify(v, null, 2), since PHP's JSON_PRETTY_PRINT hardcodes 4 spaces.
* Objects (stdClass or string-keyed arrays) keep insertion order. */
function cfcPretty(mixed $v): string
{
return cfcWriteValue($v, 0) . "\n";
}
function cfcWriteValue(mixed $v, int $depth): string
{
if ($v === null) {
return 'null';
}
if (is_bool($v)) {
return $v ? 'true' : 'false';
}
if (is_int($v) || is_float($v)) {
return cfcFormatNumber($v);
}
if (is_string($v)) {
return cfcEscapeString($v);
}
$pad = str_repeat(' ', $depth + 1);
$close = str_repeat(' ', $depth);
if ($v instanceof stdClass) {
$props = get_object_vars($v);
if ($props === []) {
return '{}';
}
$parts = [];
foreach ($props as $k => $val) {
$parts[] = $pad . cfcEscapeString((string) $k) . ': ' . cfcWriteValue($val, $depth + 1);
}
return "{\n" . implode(",\n", $parts) . "\n" . $close . '}';
}
if (is_array($v)) {
if ($v === []) {
return '[]';
}
if (!array_is_list($v)) {
// A string-keyed array is an object in the making.
$parts = [];
foreach ($v as $k => $val) {
$parts[] = $pad . cfcEscapeString((string) $k) . ': ' . cfcWriteValue($val, $depth + 1);
}
return "{\n" . implode(",\n", $parts) . "\n" . $close . '}';
}
$parts = [];
foreach ($v as $val) {
$parts[] = $pad . cfcWriteValue($val, $depth + 1);
}
return "[\n" . implode(",\n", $parts) . "\n" . $close . ']';
}
return 'null';
}
function cfcFormatNumber(int|float $n): string
{
if (is_int($n)) {
return (string) $n;
}
// Whole floats render without a trailing ".0", the way JS coerces them.
if ($n == floor($n) && abs($n) < 1e15) {
return sprintf('%.0f', $n);
}
return (string) $n;
}
function cfcEscapeString(string $s): string
{
$map = ['"' => '\\"', '\\' => '\\\\', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t',
"\x08" => '\\b', "\x0c" => '\\f'];
$out = '"';
foreach (preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY) as $c) {
if (isset($map[$c])) {
$out .= $map[$c];
} elseif (strlen($c) === 1 && ord($c) < 0x20) {
$out .= sprintf('\\u%04x', ord($c));
} else {
$out .= $c;
}
}
return $out . '"';
}
/* ---------------------------------- parse --------------------------------- */
/** OpenAI messages[] (bare array, or wrapped in {"messages": [...]}). */
function cfcParseOpenAI(string $text, array &$warnings): array
{
try {
$parsed = json_decode($text, false, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new InvalidArgumentException('OpenAI transcript: invalid JSON — ' . $e->getMessage());
}
$root = cfcAsRecord($parsed);
$raw = ($root !== null && ($msgs = cfcAsArray($root->messages ?? null)) !== null) ? $msgs : $parsed;
if (!is_array($raw)) {
throw new InvalidArgumentException('OpenAI transcript must be a messages[] array or an object with a "messages" array');
}
$out = [];
foreach ($raw as $i => $entry) {
$m = cfcAsRecord($entry);
if ($m === null) {
throw new InvalidArgumentException("messages[{$i}] is not an object");
}
$role = $m->role ?? null;
if ($role === 'system' || $role === 'developer') {
$out[] = new InternalMessage('system', cfcContentText($m->content ?? null, "messages[{$i}]", $warnings));
continue;
}
if ($role === 'user' || $role === 'assistant' || $role === 'tool') {
$msg = new InternalMessage($role, cfcContentText($m->content ?? null, "messages[{$i}]", $warnings));
if (is_string($m->name ?? null)) {
$msg->name = $m->name;
}
if (is_string($m->tool_call_id ?? null)) {
$msg->toolCallId = $m->tool_call_id;
}
$calls = cfcParseOpenAIToolCalls($m->tool_calls ?? null);
if ($calls !== null) {
$msg->toolCalls = $calls;
}
$out[] = $msg;
continue;
}
throw new InvalidArgumentException("messages[{$i}] has unsupported role: " . json_encode($role));
}
return $out;
}
/** OpenAI content → plain text (string, or text parts joined; other parts flagged). */
function cfcContentText(mixed $content, string $where, array &$warnings): string
{
if (is_string($content)) {
return $content;
}
if ($content === null) {
return '';
}
if (is_array($content)) {
$texts = [];
$dropped = 0;
foreach ($content as $part) {
$p = cfcAsRecord($part);
if ($p !== null && ($p->type ?? null) === 'text' && is_string($p->text ?? null)) {
$texts[] = $p->text;
} else {
$dropped++;
}
}
if ($dropped > 0) {
$warnings[] = "{$where}: dropped {$dropped} non-text content part(s)";
}
return implode('', $texts);
}
throw new InvalidArgumentException("{$where}: content must be a string, an array of parts, or null");
}
/** @return list<ToolCall>|null */
function cfcParseOpenAIToolCalls(mixed $raw): ?array
{
if ($raw === null) {
return null;
}
if (!is_array($raw)) {
throw new InvalidArgumentException('tool_calls must be an array');
}
$calls = [];
foreach ($raw as $entry) {
$tc = cfcAsRecord($entry);
$fn = $tc !== null ? cfcAsRecord($tc->function ?? null) : null;
$calls[] = new ToolCall(
id: ($tc !== null && is_string($tc->id ?? null)) ? $tc->id : '',
name: ($fn !== null && is_string($fn->name ?? null)) ? $fn->name : '',
args: ($fn !== null && is_string($fn->arguments ?? null)) ? $fn->arguments : '',
);
}
return $calls;
}
/** Anthropic messages[] (+ top-level `system`). */
function cfcParseAnthropic(string $text, array &$warnings): array
{
try {
$root = cfcAsRecord(json_decode($text, false, 512, JSON_THROW_ON_ERROR));
} catch (JsonException $e) {
throw new InvalidArgumentException('Anthropic transcript: invalid JSON — ' . $e->getMessage());
}
if ($root === null || !is_array($root->messages ?? null)) {
throw new InvalidArgumentException('Anthropic transcript must be an object with a "messages" array');
}
$out = [];
if (property_exists($root, 'system')) {
$out[] = new InternalMessage('system', cfcBlocksToText($root->system, 'system', $warnings));
}
foreach ($root->messages as $i => $entry) {
$m = cfcAsRecord($entry);
if ($m === null) {
throw new InvalidArgumentException("messages[{$i}] is not an object");
}
$where = "messages[{$i}]";
if (($m->role ?? null) === 'assistant') {
$texts = [];
$calls = [];
foreach (cfcContentBlocks($m->content ?? null, $where) as [$kind, $raw]) {
if ($kind === 'text') {
$texts[] = $raw;
} elseif ($kind === 'tool_use') {
$calls[] = new ToolCall(
id: is_string($raw->id ?? null) ? $raw->id : '',
name: is_string($raw->name ?? null) ? $raw->name : '',
args: cfcStringify(cfcOrEmptyObject($raw->input ?? null)),
);
} elseif ($kind === 'other') {
$warnings[] = "{$where}: dropped unsupported " . cfcBlockTypeName($raw) . ' block';
}
}
$msg = new InternalMessage('assistant', implode("\n", $texts));
if ($calls !== []) {
$msg->toolCalls = $calls;
}
$out[] = $msg;
continue;
}
if (($m->role ?? null) === 'user') {
$texts = [];
$flush = function () use (&$out, &$texts): void {
if ($texts !== []) {
$out[] = new InternalMessage('user', implode("\n", $texts));
$texts = [];
}
};
foreach (cfcContentBlocks($m->content ?? null, $where) as [$kind, $raw]) {
if ($kind === 'text') {
$texts[] = $raw;
} elseif ($kind === 'tool_use') {
$warnings[] = "{$where}: tool_use block inside a user message moved to an assistant tool call";
$flush();
$out[] = new InternalMessage('assistant', '', toolCalls: [new ToolCall(
id: is_string($raw->id ?? null) ? $raw->id : '',
name: is_string($raw->name ?? null) ? $raw->name : '',
args: cfcStringify(cfcOrEmptyObject($raw->input ?? null)),
)]);
} elseif ($kind === 'tool_result') {
$flush();
$out[] = new InternalMessage(
'tool',
cfcBlocksToText($raw->content ?? '', $where, $warnings),
toolCallId: is_string($raw->tool_use_id ?? null) ? $raw->tool_use_id : '',
);
} else {
$warnings[] = "{$where}: dropped unsupported " . cfcBlockTypeName($raw) . ' block';
}
}
$flush();
continue;
}
throw new InvalidArgumentException("{$where} has unsupported role: " . json_encode($m->role ?? null));
}
return $out;
}
/**
* Normalize Anthropic content (string | block list) into typed pairs:
* ['text', string] | ['tool_use'|'tool_result'|'other', stdClass].
*
* @return list<array{0: string, 1: mixed}>
*/
function cfcContentBlocks(mixed $content, string $where): array
{
if (is_string($content)) {
return [['text', $content]];
}
if (!is_array($content)) {
throw new InvalidArgumentException("{$where}: content must be a string or an array of blocks");
}
$blocks = [];
foreach ($content as $block) {
$b = cfcAsRecord($block);
if ($b !== null && ($b->type ?? null) === 'text' && is_string($b->text ?? null)) {
$blocks[] = ['text', $b->text];
} elseif ($b !== null && in_array($b->type ?? null, ['tool_use', 'tool_result'], true)) {
$blocks[] = [$b->type, $b];
} else {
$blocks[] = ['other', $b ?? new stdClass()];
}
}
return $blocks;
}
function cfcBlockTypeName(stdClass $raw): string
{
$t = $raw->type ?? null;
return is_string($t) ? $t : 'content';
}
/** Anthropic string-or-block-list content → plain text. */
function cfcBlocksToText(mixed $content, string $where, array &$warnings): string
{
if (is_string($content)) {
return $content;
}
$texts = [];
foreach (cfcContentBlocks($content, $where) as [$kind, $raw]) {
if ($kind === 'text') {
$texts[] = $raw;
} elseif ($kind === 'other') {
$warnings[] = "{$where}: dropped unsupported " . cfcBlockTypeName($raw) . ' block';
} else {
$warnings[] = "{$where}: dropped {$kind} block from text-only content";
}
}
return implode("\n", $texts);
}
/** Gemini contents[] (+ optional systemInstruction). */
function cfcParseGemini(string $text, array &$warnings): array
{
try {
$root = cfcAsRecord(json_decode($text, false, 512, JSON_THROW_ON_ERROR));
} catch (JsonException $e) {
throw new InvalidArgumentException('Gemini transcript: invalid JSON — ' . $e->getMessage());
}
if ($root === null || !is_array($root->contents ?? null)) {
throw new InvalidArgumentException('Gemini transcript must be an object with a "contents" array');
}
$out = [];
if (property_exists($root, 'systemInstruction')) {
$out[] = new InternalMessage('system', cfcGeminiText($root->systemInstruction, 'systemInstruction', $warnings));
}
foreach ($root->contents as $i => $entry) {
$m = cfcAsRecord($entry);
if ($m === null) {
throw new InvalidArgumentException("contents[{$i}] is not an object");
}
$where = "contents[{$i}]";
if (($m->role ?? null) !== 'user' && ($m->role ?? null) !== 'model') {
throw new InvalidArgumentException(
"{$where} has unsupported role: " . json_encode($m->role ?? null) . ' (Gemini uses "user" or "model")'
);
}
$role = $m->role === 'model' ? 'assistant' : 'user';
$parts = cfcAsArray($m->parts ?? null);
if ($parts === null) {
throw new InvalidArgumentException("{$where}: parts must be an array");
}
if ($role === 'assistant') {
// A model turn keeps its text and function calls in ONE message,
// mirroring an OpenAI assistant message with tool_calls.
$texts = [];
$calls = [];
foreach ($parts as $part) {
$p = cfcAsRecord($part);
if ($p !== null && is_string($p->text ?? null)) {
$texts[] = $p->text;
continue;
}
$fc = $p !== null ? cfcAsRecord($p->functionCall ?? null) : null;
if ($fc !== null) {
$calls[] = new ToolCall(
name: is_string($fc->name ?? null) ? $fc->name : '',
args: cfcStringify(cfcOrEmptyObject($fc->args ?? null)),
);
continue;
}
$warnings[] = "{$where}: dropped unsupported part (inlineData or similar)";
}
$msg = new InternalMessage('assistant', implode("\n", $texts));
if ($calls !== []) {
$msg->toolCalls = $calls;
}
$out[] = $msg;
continue;
}
$texts = [];
$flushText = function () use (&$out, &$texts): void {
if ($texts !== []) {
$out[] = new InternalMessage('user', implode("\n", $texts));
$texts = [];
}
};
foreach ($parts as $part) {
$p = cfcAsRecord($part);
if ($p !== null && is_string($p->text ?? null)) {
$texts[] = $p->text;
continue;
}
$fc = $p !== null ? cfcAsRecord($p->functionCall ?? null) : null;
if ($fc !== null) {
$flushText();
$out[] = new InternalMessage('assistant', '', toolCalls: [new ToolCall(
name: is_string($fc->name ?? null) ? $fc->name : '',
args: cfcStringify(cfcOrEmptyObject($fc->args ?? null)),
)]);
continue;
}
$fr = $p !== null ? cfcAsRecord($p->functionResponse ?? null) : null;
if ($fr !== null) {
$flushText();
$name = is_string($fr->name ?? null) ? $fr->name : '';
$out[] = new InternalMessage(
'tool',
cfcStringify(cfcOrEmptyObject($fr->response ?? null)),
$name,
null,
$name,
);
continue;
}
$warnings[] = "{$where}: dropped unsupported part (inlineData or similar)";
}
$flushText();
}
return $out;
}
/** Gemini systemInstruction (string or {parts}) → plain text. */
function cfcGeminiText(mixed $v, string $where, array &$warnings): string
{
if (is_string($v)) {
return $v;
}
$rec = cfcAsRecord($v);
if ($rec !== null) {
if (is_string($rec->text ?? null)) {
return $rec->text;
}
if (is_array($rec->parts ?? null)) {
$texts = [];
foreach ($rec->parts as $part) {
$p = cfcAsRecord($part);
$texts[] = ($p !== null && is_string($p->text ?? null)) ? $p->text : '';
}
return implode("\n", $texts);
}
}
$warnings[] = "{$where}: unsupported systemInstruction shape, treated as empty";
return '';
}
/**
* Markdown transcript: `**role**: text` header lines with continuation lines
* belonging to the same message. `model` maps to assistant.
*/
function cfcParseMarkdown(string $text, array &$warnings): array
{
$out = [];
$current = null; // [string $role, list<string> $lines]
foreach (explode("\n", $text) as $line) {
if (preg_match('/^\*\*(system|user|assistant|model|tool)\*\*:\s*(.*)$/', $line, $m) === 1) {
if ($current !== null) {
$out[] = cfcFinishMessage($current);
}
$current = [$m[1] === 'model' ? 'assistant' : $m[1], [$m[2]]];
continue;
}
if ($current !== null) {
$current[1][] = $line;
} elseif (trim($line) !== '') {
throw new InvalidArgumentException('Markdown transcript must start with a `**role**:` header line');
}
}
if ($current !== null) {
$out[] = cfcFinishMessage($current);
}
return $out;
}
/** @param array{0: string, 1: list<string>} $current */
function cfcFinishMessage(array $current): InternalMessage
{
// Trim leading/trailing blank continuation lines but keep inner blank lines.
$lines = $current[1];
while (count($lines) > 1 && trim($lines[0]) === '') {
array_shift($lines);
}
while (count($lines) > 1 && trim($lines[count($lines) - 1]) === '') {
array_pop($lines);
}
return new InternalMessage($current[0], implode("\n", $lines));
}
/* -------------------------------- serialize ------------------------------- */
function cfcSerializeOpenAI(array $messages): string
{
$arr = [];
foreach ($messages as $m) {
if ($m->role === 'tool') {
$o = ['role' => 'tool', 'content' => $m->content, 'tool_call_id' => $m->toolCallId ?? ''];
if ($m->name !== null) {
$o['name'] = $m->name;
}
$arr[] = $o;
continue;
}
if ($m->role === 'assistant' && $m->toolCalls !== null) {
$o = [
'role' => 'assistant',
'content' => $m->content === '' ? null : $m->content,
'tool_calls' => array_map(
static fn (ToolCall $tc) => [
'id' => $tc->id,
'type' => 'function',
'function' => ['name' => $tc->name, 'arguments' => $tc->args],
],
$m->toolCalls,
),
];
if ($m->name !== null) {
$o['name'] = $m->name;
}
$arr[] = $o;
continue;
}
$o = ['role' => $m->role, 'content' => $m->content];
if ($m->name !== null) {
$o['name'] = $m->name;
}
$arr[] = $o;
}
return cfcPretty(['messages' => $arr]);
}
function cfcSerializeAnthropic(array $messages, array &$warnings): string
{
$system = [];
$out = [];
foreach ($messages as $m) {
if ($m->role === 'system') {
$system[] = $m->content;
}
}
foreach ($messages as $i => $m) {
if ($m->role === 'system') {
continue;
}
if ($m->name !== null && $m->role !== 'tool') {
$warnings[] = "message {$i}: \"name\" has no Anthropic equivalent and was dropped";
}
if ($m->role === 'user') {
$out[] = ['role' => 'user', 'content' => $m->content];
} elseif ($m->role === 'assistant') {
if ($m->toolCalls !== null) {
$blocks = [];
if ($m->content !== '') {
$blocks[] = ['type' => 'text', 'text' => $m->content];
}
foreach ($m->toolCalls as $tc) {
$blocks[] = [
'type' => 'tool_use',
'id' => $tc->id,
'name' => $tc->name,
'input' => cfcParseArgsOrEmpty($tc->args),
];
}
$out[] = ['role' => 'assistant', 'content' => $blocks];
} else {
$out[] = ['role' => 'assistant', 'content' => $m->content];
}
} else {
$out[] = [
'role' => 'user',
'content' => [['type' => 'tool_result', 'tool_use_id' => $m->toolCallId ?? '', 'content' => $m->content]],
];
}
}
$root = $out !== [] ? ['messages' => $out] : [];
if ($system !== []) {
$root['system'] = implode("\n\n", $system);
}
return cfcPretty($root ?: new stdClass());
}
function cfcSerializeGemini(array $messages, array &$warnings): string
{
$system = [];
foreach ($messages as $m) {
if ($m->role === 'system') {
$system[] = $m->content;
}
}
$contents = [];
$push = function (string $role, array $part) use (&$contents): void {
$last = count($contents) - 1;
if ($last >= 0 && $contents[$last]['role'] === $role) {
$contents[$last]['parts'][] = $part;
} else {
$contents[] = ['role' => $role, 'parts' => [$part]];
}
};
foreach ($messages as $i => $m) {
if ($m->role === 'system') {
continue;
}
if ($m->name !== null && $m->role !== 'tool') {
$warnings[] = "message {$i}: \"name\" has no Gemini equivalent and was dropped";
}
if ($m->role === 'user') {
$push('user', ['text' => $m->content]);
} elseif ($m->role === 'assistant') {
if ($m->content !== '') {
$push('model', ['text' => $m->content]);
}
foreach ($m->toolCalls ?? [] as $tc) {
$push('model', ['functionCall' => ['name' => $tc->name, 'args' => cfcParseArgsOrEmpty($tc->args)]]);
}
} else {
$push('user', [
'functionResponse' => [
'name' => $m->name ?? $m->toolCallId ?? '',
'response' => cfcParseArgsOrEmpty($m->content),
],
]);
}
}
$root = $contents !== [] ? ['contents' => $contents] : [];
if ($system !== []) {
$root['systemInstruction'] = ['parts' => [['text' => implode("\n\n", $system)]]];
}
return cfcPretty($root ?: new stdClass());
}
function cfcSerializeMarkdown(array $messages, array &$warnings): string
{
foreach ($messages as $i => $m) {
if ($m->toolCalls !== null && $m->toolCalls !== []) {
$warnings[] = "message {$i}: tool calls are not representable in Markdown and were dropped";
}
if ($m->role === 'tool' && $m->toolCallId !== null) {
$warnings[] = "message {$i}: tool result id is not representable in Markdown and was dropped";
}
}
return implode("\n\n", array_map(
static fn (InternalMessage $m) => "**{$m->role}**: {$m->content}",
$messages,
)) . "\n";
}
function cfcParsers(): array
{
return [
'openai' => 'cfcParseOpenAI',
'anthropic' => 'cfcParseAnthropic',
'gemini' => 'cfcParseGemini',
'markdown' => 'cfcParseMarkdown',
];
}
function cfcSerializers(): array
{
return [
'openai' => 'cfcSerializeOpenAI',
'anthropic' => 'cfcSerializeAnthropic',
'gemini' => 'cfcSerializeGemini',
'markdown' => 'cfcSerializeMarkdown',
];
}
/**
* Convert a transcript between chat formats through the shared internal shape.
*
* @param list<string> $warnings populated with notes about unmapped fields.
*
* @throws InvalidArgumentException when the transcript is empty, the source
* format fails to parse, or $from/$to is not a known format.
*/
function cfcConvert(string $transcript, string $from, string $to, ?array &$warnings = null): ConversionResult
{
$warnings = $warnings ?? [];
if (trim($transcript) === '') {
throw new InvalidArgumentException('Transcript is empty — paste a transcript first');
}
$parsers = cfcParsers();
if (!isset($parsers[$from])) {
throw new InvalidArgumentException("Unknown source format: {$from}");
}
$serializers = cfcSerializers();
if (!isset($serializers[$to])) {
throw new InvalidArgumentException("Unknown target format: {$to}");
}
$messages = $parsers[$from]($transcript, $warnings);
$output = $serializers[$to]($messages, $warnings);
return new ConversionResult($output, $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 →