Chat Format Converter — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// chat-format-converter — Go polyglot showcase port (core conversion logic).
//
// The authoritative, runnable Go source is the live CLI twin at
// cli/chat-format-converter/chatformatconverter.go (cosmodev chat-format-converter) —
// a full port with an order-preserving JSON encoder for byte-exact output.
// This excerpt carries the core conversion rules: parse one chat transcript
// (OpenAI / Anthropic / Gemini / Markdown) into a shared internal message
// list, then serialize it into any target shape. Fields a target format
// cannot represent are flagged as warnings, never dropped silently.
//
// Idiomatic Go condensed port: encoding/json maps are used for the JSON tree,
// so re-serialized keys come out sorted rather than in insertion order — the
// live twin carries the order-preserving writer when byte-exactness matters.
// Standard library only.
//
// This is the Go sibling of src/lib/chatFormatConverter.ts (the canonical
// TypeScript that powers the live tool).
// Display source — part of CosmoDev's polyglot tool pages (dev.cosmolabs.org).
package chatformatconverter
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
)
// ChatFormat is one transcript shape: openai | anthropic | gemini | markdown.
type ChatFormat = string
const (
FormatOpenAI ChatFormat = "openai"
FormatAnthropic ChatFormat = "anthropic"
FormatGemini ChatFormat = "gemini"
FormatMarkdown ChatFormat = "markdown"
)
// ChatFormats lists every supported format.
var ChatFormats = []ChatFormat{FormatOpenAI, FormatAnthropic, FormatGemini, FormatMarkdown}
// ToolCall is one assistant-emitted tool call, provider-agnostic.
// Args is a JSON string.
type ToolCall struct {
ID string
Name string
Args string
}
// InternalMessage is the shared internal shape every format parses into and
// serializes from.
type InternalMessage struct {
Role string // "system" | "user" | "assistant" | "tool"
Content string // '' when the message only carries tool calls/results
Name string // OpenAI name, Anthropic tool_use, Gemini functionResponse
ToolCalls []ToolCall
ToolCallID string // OpenAI tool_call_id, Anthropic tool_use_id, Gemini name
}
// ConversionResult carries the serialized output plus human-readable notes
// about fields that could not be mapped.
type ConversionResult struct {
Output string
Warnings []string
}
/* --------------------------------- helpers -------------------------------- */
func parseTranscriptJSON(text, label string) (any, error) {
var v any
if err := json.Unmarshal([]byte(text), &v); err != nil {
return nil, fmt.Errorf("%s: invalid JSON — %s", label, err)
}
return v, nil
}
func asObject(v any) map[string]any {
m, _ := v.(map[string]any)
return m
}
// orEmpty mirrors the TS `?? {}` capture: null/missing becomes {}, any other
// value (string, number, ...) passes through untouched.
func orEmpty(v any) any {
if v == nil {
return map[string]any{}
}
return v
}
// parseArgsOrEmpty parses a JSON-string argument back to a value, an empty
// object when it is not a JSON object.
func parseArgsOrEmpty(args string) any {
var v any
if err := json.Unmarshal([]byte(args), &v); err == nil {
if m, ok := v.(map[string]any); ok {
return m
}
}
return map[string]any{}
}
func encodeCompact(v any) string {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
_ = enc.Encode(v)
return strings.TrimRight(b.String(), "\n")
}
func encodePretty(v any) string {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
_ = enc.Encode(v)
return b.String() // Encoder.Encode already appends the trailing newline
}
/* ---------------------------------- parse --------------------------------- */
// parseOpenAI reads OpenAI messages[] (bare array, or wrapped in
// {"messages": [...]}).
func parseOpenAI(text string, warnings *[]string) ([]InternalMessage, error) {
v, err := parseTranscriptJSON(text, "OpenAI transcript")
if err != nil {
return nil, err
}
root := asObject(v)
raw := v
if root != nil {
if msgs, ok := root["messages"].([]any); ok {
raw = msgs
}
}
arr, ok := raw.([]any)
if !ok {
return nil, errors.New(`OpenAI transcript must be a messages[] array or an object with a "messages" array`)
}
out := []InternalMessage{}
for i, entry := range arr {
m := asObject(entry)
if m == nil {
return nil, fmt.Errorf("messages[%d] is not an object", i)
}
role, _ := m["role"].(string)
where := fmt.Sprintf("messages[%d]", i)
switch role {
case "system", "developer":
content, err := contentText(m["content"], where, warnings)
if err != nil {
return nil, err
}
out = append(out, InternalMessage{Role: "system", Content: content})
case "user", "assistant", "tool":
content, err := contentText(m["content"], where, warnings)
if err != nil {
return nil, err
}
msg := InternalMessage{Role: role, Content: content}
if name, ok := m["name"].(string); ok {
msg.Name = name
}
if id, ok := m["tool_call_id"].(string); ok {
msg.ToolCallID = id
}
calls, err := parseOpenAIToolCalls(m["tool_calls"])
if err != nil {
return nil, err
}
if calls != nil {
msg.ToolCalls = calls
}
out = append(out, msg)
default:
return nil, fmt.Errorf("messages[%d] has unsupported role: %s", i, encodeCompact(m["role"]))
}
}
return out, nil
}
// contentText flattens OpenAI content to plain text (string, or text parts
// joined; other parts flagged as warnings).
func contentText(content any, where string, warnings *[]string) (string, error) {
switch c := content.(type) {
case string:
return c, nil
case nil:
return "", nil
case []any:
texts := []string{}
dropped := 0
for _, part := range c {
p := asObject(part)
if t, ok := p["text"].(string); ok && p["type"] == "text" && p != nil {
texts = append(texts, t)
} else {
dropped++
}
}
if dropped > 0 {
*warnings = append(*warnings, fmt.Sprintf("%s: dropped %d non-text content part(s)", where, dropped))
}
return strings.Join(texts, ""), nil
default:
return "", fmt.Errorf("%s: content must be a string, an array of parts, or null", where)
}
}
func parseOpenAIToolCalls(raw any) ([]ToolCall, error) {
if raw == nil {
return nil, nil
}
list, ok := raw.([]any)
if !ok {
return nil, errors.New("tool_calls must be an array")
}
calls := make([]ToolCall, 0, len(list))
for _, entry := range list {
tc := asObject(entry)
fn := map[string]any{}
if tc != nil {
fn = asObject(tc["function"])
}
call := ToolCall{}
if tc != nil {
if id, ok := tc["id"].(string); ok {
call.ID = id
}
}
if fn != nil {
if name, ok := fn["name"].(string); ok {
call.Name = name
}
if args, ok := fn["arguments"].(string); ok {
call.Args = args
}
}
calls = append(calls, call)
}
return calls, nil
}
// anthBlock is one normalized Anthropic content block.
type anthBlock struct {
kind string // "text" | "tool_use" | "tool_result" | "other"
text string
raw map[string]any
}
func blockTypeName(raw map[string]any) string {
if t, ok := raw["type"].(string); ok {
return t
}
return "content"
}
func toolCallFromBlock(raw map[string]any) ToolCall {
call := ToolCall{Args: encodeCompact(orEmpty(raw["input"]))}
if id, ok := raw["id"].(string); ok {
call.ID = id
}
if name, ok := raw["name"].(string); ok {
call.Name = name
}
return call
}
// contentBlocks normalizes Anthropic content (string | block[]) into typed blocks.
func contentBlocks(content any, where string) ([]anthBlock, error) {
if s, ok := content.(string); ok {
return []anthBlock{{kind: "text", text: s}}, nil
}
list, ok := content.([]any)
if !ok {
return nil, fmt.Errorf("%s: content must be a string or an array of blocks", where)
}
blocks := make([]anthBlock, 0, len(list))
for _, block := range list {
b := asObject(block)
if b != nil {
if t, ok := b["text"].(string); ok && b["type"] == "text" {
blocks = append(blocks, anthBlock{kind: "text", text: t})
continue
}
if b["type"] == "tool_use" || b["type"] == "tool_result" {
kind, _ := b["type"].(string)
blocks = append(blocks, anthBlock{kind: kind, raw: b})
continue
}
}
blocks = append(blocks, anthBlock{kind: "other", raw: b}) // b may be nil; blockTypeName handles it
}
return blocks, nil
}
// blocksToText flattens Anthropic string-or-block[] content to plain text.
func blocksToText(content any, where string, warnings *[]string) (string, error) {
if s, ok := content.(string); ok {
return s, nil
}
blocks, err := contentBlocks(content, where)
if err != nil {
return "", err
}
texts := []string{}
for _, b := range blocks {
switch b.kind {
case "text":
texts = append(texts, b.text)
case "other":
*warnings = append(*warnings, fmt.Sprintf("%s: dropped unsupported %s block", where, blockTypeName(b.raw)))
default:
*warnings = append(*warnings, fmt.Sprintf("%s: dropped %s block from text-only content", where, b.kind))
}
}
return strings.Join(texts, "\n"), nil
}
// parseAnthropic reads Anthropic messages[] plus the top-level system field.
func parseAnthropic(text string, warnings *[]string) ([]InternalMessage, error) {
v, err := parseTranscriptJSON(text, "Anthropic transcript")
if err != nil {
return nil, err
}
root := asObject(v)
if root == nil {
return nil, errors.New(`Anthropic transcript must be an object with a "messages" array`)
}
msgList, ok := root["messages"].([]any)
if !ok {
return nil, errors.New(`Anthropic transcript must be an object with a "messages" array`)
}
out := []InternalMessage{}
if sys, present := root["system"]; present {
content, err := blocksToText(sys, "system", warnings)
if err != nil {
return nil, err
}
out = append(out, InternalMessage{Role: "system", Content: content})
}
for i, entry := range msgList {
m := asObject(entry)
if m == nil {
return nil, fmt.Errorf("messages[%d] is not an object", i)
}
where := fmt.Sprintf("messages[%d]", i)
switch m["role"] {
case "assistant":
texts := []string{}
var calls []ToolCall
blocks, err := contentBlocks(m["content"], where)
if err != nil {
return nil, err
}
for _, b := range blocks {
switch b.kind {
case "text":
texts = append(texts, b.text)
case "tool_use":
calls = append(calls, toolCallFromBlock(b.raw))
case "other":
*warnings = append(*warnings, fmt.Sprintf("%s: dropped unsupported %s block", where, blockTypeName(b.raw)))
}
}
msg := InternalMessage{Role: "assistant", Content: strings.Join(texts, "\n")}
if len(calls) > 0 {
msg.ToolCalls = calls
}
out = append(out, msg)
case "user":
texts := []string{}
flush := func() {
if len(texts) > 0 {
out = append(out, InternalMessage{Role: "user", Content: strings.Join(texts, "\n")})
texts = texts[:0]
}
}
blocks, err := contentBlocks(m["content"], where)
if err != nil {
return nil, err
}
for _, b := range blocks {
switch b.kind {
case "text":
texts = append(texts, b.text)
case "tool_use":
*warnings = append(*warnings, fmt.Sprintf("%s: tool_use block inside a user message moved to an assistant tool call", where))
flush()
out = append(out, InternalMessage{Role: "assistant", ToolCalls: []ToolCall{toolCallFromBlock(b.raw)}})
case "tool_result":
flush()
content, err := blocksToText(orEmptyString(b.raw["content"]), where, warnings)
if err != nil {
return nil, err
}
msg := InternalMessage{Role: "tool", Content: content}
if id, ok := b.raw["tool_use_id"].(string); ok {
msg.ToolCallID = id
}
out = append(out, msg)
default:
*warnings = append(*warnings, fmt.Sprintf("%s: dropped unsupported %s block", where, blockTypeName(b.raw)))
}
}
flush()
default:
return nil, fmt.Errorf("%s has unsupported role: %s", where, encodeCompact(m["role"]))
}
}
return out, nil
}
func orEmptyString(v any) any {
if v == nil {
return ""
}
return v
}
// geminiText flattens a Gemini systemInstruction (string or {parts}) to text.
func geminiText(v any, where string, warnings *[]string) string {
if s, ok := v.(string); ok {
return s
}
if rec := asObject(v); rec != nil {
if t, ok := rec["text"].(string); ok {
return t
}
if parts, ok := rec["parts"].([]any); ok {
texts := make([]string, 0, len(parts))
for _, part := range parts {
p := asObject(part)
if t, ok := p["text"].(string); ok && p != nil {
texts = append(texts, t)
} else {
texts = append(texts, "")
}
}
return strings.Join(texts, "\n")
}
}
*warnings = append(*warnings, fmt.Sprintf("%s: unsupported systemInstruction shape, treated as empty", where))
return ""
}
// parseGemini reads Gemini contents[] plus the optional systemInstruction.
func parseGemini(text string, warnings *[]string) ([]InternalMessage, error) {
v, err := parseTranscriptJSON(text, "Gemini transcript")
if err != nil {
return nil, err
}
root := asObject(v)
if root == nil {
return nil, errors.New(`Gemini transcript must be an object with a "contents" array`)
}
contents, ok := root["contents"].([]any)
if !ok {
return nil, errors.New(`Gemini transcript must be an object with a "contents" array`)
}
out := []InternalMessage{}
if si, present := root["systemInstruction"]; present {
out = append(out, InternalMessage{Role: "system", Content: geminiText(si, "systemInstruction", warnings)})
}
for i, entry := range contents {
m := asObject(entry)
if m == nil {
return nil, fmt.Errorf("contents[%d] is not an object", i)
}
where := fmt.Sprintf("contents[%d]", i)
role, _ := m["role"].(string)
if role != "user" && role != "model" {
return nil, fmt.Errorf("%s has unsupported role: %s (Gemini uses \"user\" or \"model\")", where, encodeCompact(m["role"]))
}
parts, ok := m["parts"].([]any)
if !ok {
return nil, fmt.Errorf("%s: parts must be an array", where)
}
if role == "model" {
// A model turn keeps its text and function calls in ONE message,
// mirroring an OpenAI assistant message with tool_calls.
texts := []string{}
var calls []ToolCall
for _, part := range parts {
p := asObject(part)
if t, ok := p["text"].(string); ok && p != nil {
texts = append(texts, t)
continue
}
if fc := asObject(p["functionCall"]); fc != nil && p != nil {
call := ToolCall{Args: encodeCompact(orEmpty(fc["args"]))}
if name, ok := fc["name"].(string); ok {
call.Name = name
}
calls = append(calls, call)
continue
}
*warnings = append(*warnings, fmt.Sprintf("%s: dropped unsupported part (inlineData or similar)", where))
}
msg := InternalMessage{Role: "assistant", Content: strings.Join(texts, "\n")}
if len(calls) > 0 {
msg.ToolCalls = calls
}
out = append(out, msg)
continue
}
texts := []string{}
flushText := func() {
if len(texts) > 0 {
out = append(out, InternalMessage{Role: "user", Content: strings.Join(texts, "\n")})
texts = texts[:0]
}
}
for _, part := range parts {
p := asObject(part)
if t, ok := p["text"].(string); ok && p != nil {
texts = append(texts, t)
continue
}
if fc := asObject(p["functionCall"]); fc != nil && p != nil {
flushText()
call := ToolCall{Args: encodeCompact(orEmpty(fc["args"]))}
if name, ok := fc["name"].(string); ok {
call.Name = name
}
out = append(out, InternalMessage{Role: "assistant", ToolCalls: []ToolCall{call}})
continue
}
if fr := asObject(p["functionResponse"]); fr != nil && p != nil {
flushText()
name, _ := fr["name"].(string)
out = append(out, InternalMessage{
Role: "tool",
Content: encodeCompact(orEmpty(fr["response"])),
Name: name,
ToolCallID: name,
})
continue
}
*warnings = append(*warnings, fmt.Sprintf("%s: dropped unsupported part (inlineData or similar)", where))
}
flushText()
}
return out, nil
}
var markdownHeader = regexp.MustCompile(`^\*\*(system|user|assistant|model|tool)\*\*:\s*(.*)$`)
// markdownPartial is the in-flight message while scanning a Markdown transcript.
type markdownPartial struct {
role string
lines []string
}
func finishMarkdownMessage(current *markdownPartial) InternalMessage {
// Trim leading/trailing blank continuation lines but keep inner blank lines.
for len(current.lines) > 1 && strings.TrimSpace(current.lines[0]) == "" {
current.lines = current.lines[1:]
}
for len(current.lines) > 1 && strings.TrimSpace(current.lines[len(current.lines)-1]) == "" {
current.lines = current.lines[:len(current.lines)-1]
}
return InternalMessage{Role: current.role, Content: strings.Join(current.lines, "\n")}
}
// parseMarkdown reads a `**role**: text` transcript with continuation lines
// belonging to the same message. `model` maps to assistant.
func parseMarkdown(text string, _ *[]string) ([]InternalMessage, error) {
out := []InternalMessage{}
var current *markdownPartial
for line := range strings.SplitSeq(text, "\n") {
if m := markdownHeader.FindStringSubmatch(line); m != nil {
if current != nil {
out = append(out, finishMarkdownMessage(current))
}
role := m[1]
if role == "model" {
role = "assistant"
}
current = &markdownPartial{role: role, lines: []string{m[2]}}
continue
}
if current != nil {
current.lines = append(current.lines, line)
} else if strings.TrimSpace(line) != "" {
return nil, errors.New("Markdown transcript must start with a `**role**:` header line")
}
}
if current != nil {
out = append(out, finishMarkdownMessage(current))
}
return out, nil
}
/* -------------------------------- serialize ------------------------------- */
func serializeOpenAI(messages []InternalMessage, _ *[]string) string {
arr := make([]any, 0, len(messages))
for _, m := range messages {
switch {
case m.Role == "tool":
o := map[string]any{"role": "tool", "content": m.Content, "tool_call_id": m.ToolCallID}
if m.Name != "" {
o["name"] = m.Name
}
arr = append(arr, o)
case m.Role == "assistant" && m.ToolCalls != nil:
calls := make([]any, 0, len(m.ToolCalls))
for _, tc := range m.ToolCalls {
calls = append(calls, map[string]any{
"id": tc.ID,
"type": "function",
"function": map[string]any{"name": tc.Name, "arguments": tc.Args},
})
}
content := any(m.Content)
if m.Content == "" {
content = nil
}
o := map[string]any{"role": "assistant", "content": content, "tool_calls": calls}
if m.Name != "" {
o["name"] = m.Name
}
arr = append(arr, o)
default:
o := map[string]any{"role": m.Role, "content": m.Content}
if m.Name != "" {
o["name"] = m.Name
}
arr = append(arr, o)
}
}
return encodePretty(map[string]any{"messages": arr})
}
func serializeAnthropic(messages []InternalMessage, warnings *[]string) string {
system := []string{}
for _, m := range messages {
if m.Role == "system" {
system = append(system, m.Content)
}
}
out := []any{}
for i, m := range messages {
if m.Role == "system" {
continue
}
if m.Name != "" && m.Role != "tool" {
*warnings = append(*warnings, fmt.Sprintf("message %d: %q has no Anthropic equivalent and was dropped", i, "name"))
}
switch m.Role {
case "user":
out = append(out, map[string]any{"role": "user", "content": m.Content})
case "assistant":
if m.ToolCalls != nil {
blocks := []any{}
if m.Content != "" {
blocks = append(blocks, map[string]any{"type": "text", "text": m.Content})
}
for _, tc := range m.ToolCalls {
blocks = append(blocks, map[string]any{
"type": "tool_use",
"id": tc.ID,
"name": tc.Name,
"input": parseArgsOrEmpty(tc.Args),
})
}
out = append(out, map[string]any{"role": "assistant", "content": blocks})
} else {
out = append(out, map[string]any{"role": "assistant", "content": m.Content})
}
default: // tool result: Anthropic carries it in a user turn
out = append(out, map[string]any{
"role": "user",
"content": []any{map[string]any{
"type": "tool_result",
"tool_use_id": m.ToolCallID,
"content": m.Content,
}},
})
}
}
root := map[string]any{}
if len(out) > 0 {
root["messages"] = out
}
if len(system) > 0 {
root["system"] = strings.Join(system, "\n\n")
}
return encodePretty(root)
}
func serializeGemini(messages []InternalMessage, warnings *[]string) string {
system := []string{}
for _, m := range messages {
if m.Role == "system" {
system = append(system, m.Content)
}
}
type turn struct {
role string
parts []any
}
turns := []*turn{}
push := func(role string, part any) {
if n := len(turns); n > 0 && turns[n-1].role == role {
turns[n-1].parts = append(turns[n-1].parts, part)
} else {
turns = append(turns, &turn{role: role, parts: []any{part}})
}
}
for i, m := range messages {
if m.Role == "system" {
continue
}
if m.Name != "" && m.Role != "tool" {
*warnings = append(*warnings, fmt.Sprintf("message %d: %q has no Gemini equivalent and was dropped", i, "name"))
}
switch m.Role {
case "user":
push("user", map[string]any{"text": m.Content})
case "assistant":
if m.Content != "" {
push("model", map[string]any{"text": m.Content})
}
for _, tc := range m.ToolCalls {
push("model", map[string]any{
"functionCall": map[string]any{"name": tc.Name, "args": parseArgsOrEmpty(tc.Args)},
})
}
default: // tool result: Gemini carries it as a user functionResponse
name := m.ToolCallID
if m.Name != "" {
name = m.Name
}
push("user", map[string]any{
"functionResponse": map[string]any{"name": name, "response": parseArgsOrEmpty(m.Content)},
})
}
}
root := map[string]any{}
if len(turns) > 0 {
contents := make([]any, 0, len(turns))
for _, t := range turns {
contents = append(contents, map[string]any{"role": t.role, "parts": t.parts})
}
root["contents"] = contents
}
if len(system) > 0 {
root["systemInstruction"] = map[string]any{"parts": []any{map[string]any{"text": strings.Join(system, "\n\n")}}}
}
return encodePretty(root)
}
func serializeMarkdown(messages []InternalMessage, warnings *[]string) string {
lines := make([]string, 0, len(messages))
for i, m := range messages {
if len(m.ToolCalls) > 0 {
*warnings = append(*warnings, fmt.Sprintf("message %d: tool calls are not representable in Markdown and were dropped", i))
}
if m.Role == "tool" && m.ToolCallID != "" {
*warnings = append(*warnings, fmt.Sprintf("message %d: tool result id is not representable in Markdown and was dropped", i))
}
lines = append(lines, fmt.Sprintf("**%s**: %s", m.Role, m.Content))
}
return strings.Join(lines, "\n\n") + "\n"
}
type parserFunc func(text string, warnings *[]string) ([]InternalMessage, error)
type serializerFunc func(messages []InternalMessage, warnings *[]string) string
var parsers = map[ChatFormat]parserFunc{
FormatOpenAI: parseOpenAI,
FormatAnthropic: parseAnthropic,
FormatGemini: parseGemini,
FormatMarkdown: parseMarkdown,
}
var serializers = map[ChatFormat]serializerFunc{
FormatOpenAI: serializeOpenAI,
FormatAnthropic: serializeAnthropic,
FormatGemini: serializeGemini,
FormatMarkdown: serializeMarkdown,
}
// Convert converts a transcript between chat formats through the shared
// internal shape. It errors when the transcript is empty, the source format
// fails to parse, or from/to is not a known format.
func Convert(transcript string, from, to ChatFormat) (ConversionResult, error) {
if strings.TrimSpace(transcript) == "" {
return ConversionResult{}, errors.New("Transcript is empty — paste a transcript first")
}
parser, ok := parsers[from]
if !ok {
return ConversionResult{}, fmt.Errorf("Unknown source format: %s", from)
}
serializer, ok := serializers[to]
if !ok {
return ConversionResult{}, fmt.Errorf("Unknown target format: %s", to)
}
warnings := []string{}
messages, err := parser(transcript, &warnings)
if err != nil {
return ConversionResult{}, err
}
return ConversionResult{Output: serializer(messages, &warnings), Warnings: warnings}, nil
}
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 →