{"record":{"id":"6470de1a57733230","repo":"JuliusBrussee/caveman","slug":"corpus-provider-conversion-supports-string-or-null","errorCode":null,"errorMessage":"corpus provider conversion supports string or null content","messagePattern":"corpus provider conversion supports string or null content","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cacheengine/cachebench/corpus.go","lineNumber":734,"sourceCode":"\t\treturn anthropicCorpusBody(provider.Model, messages)\n\tcase \"bedrock\":\n\t\treturn bedrockCorpusBody(messages)\n\tcase \"gemini\":\n\t\treturn geminiCorpusBody(messages)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported corpus provider %q\", provider.Provider)\n\t}\n}\n\nfunc contentText(content json.RawMessage) (string, error) {\n\tif len(content) == 0 || bytes.Equal(bytes.TrimSpace(content), []byte(\"null\")) {\n\t\treturn \"\", nil\n\t}\n\tvar text string\n\tif err := json.Unmarshal(content, &text); err == nil {\n\t\treturn text, nil\n\t}\n\treturn \"\", errors.New(\"corpus provider conversion supports string or null content\")\n}\n\nfunc anthropicCorpusBody(model string, messages []CorpusMessage) ([]byte, error) {\n\tvar system []string\n\tconverted := make([]any, 0, len(messages))\n\tfor _, message := range messages {\n\t\ttext, err := contentText(message.Content)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch message.Role {\n\t\tcase \"system\", \"developer\":\n\t\t\tif text != \"\" {\n\t\t\t\tsystem = append(system, text)\n\t\t\t}\n\t\tcase \"assistant\":\n\t\t\tblocks := make([]any, 0, len(message.ToolCalls)+1)\n\t\t\tif text != \"\" {","sourceCodeStart":716,"sourceCodeEnd":752,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/cacheengine/cachebench/corpus.go#L716-L752","documentation":"contentText converts a CorpusMessage's content field for provider-native request bodies (e.g. Anthropic). It accepts only JSON strings or null; any other JSON shape (arrays, numbers, objects) is rejected because the corpus schema normalizes content to plain text.","triggerScenarios":"A corpus message whose Content is a JSON array (OpenAI multi-part content blocks like [{\"type\":\"text\",...}]), a bare number, or an object. This surfaces during BuildCorpusTrace/anthropicCorpusBody when converting the message for a non-OpenAI provider.","commonSituations":"Corpus ingested from OpenAI chat logs that use multi-content arrays; user-uploaded corpora with structured content; a converter that passed raw request bodies as content instead of extracting the text.","solutions":["Normalize content to a string when building the corpus: join text parts of content arrays into one string (or take the first text part)","Null non-text parts (image_url etc.) or drop those messages if text-only replay is unacceptable","Enforce string-or-null content at corpus validation time so the failure happens at load, not provider conversion"],"exampleFix":"// before\nmsg.Content = json.RawMessage(`[{\"type\":\"text\",\"text\":\"hello\"}]`) // triggers error\n\n// after\ntext := extractTextParts(msg.Content) // \"hello\"\nmsg.Content = json.RawMessage(strconv.Quote(text))","handlingStrategy":"validation","validationCode":"func isTextOrNullContent(raw json.RawMessage) bool {\n    t := bytes.TrimSpace(raw)\n    if len(t) == 0 || bytes.Equal(t, []byte(\"null\")) { return true }\n    var s string\n    return json.Unmarshal(t, &s) == nil\n}\n\nfor i := range corpus.Rows {\n    for j := range corpus.Rows[i].Messages {\n        if !isTextOrNullContent(corpus.Rows[i].Messages[j].Content) {\n            corpus.Rows[i].Messages[j].Content = normalizeToText(corpus.Rows[i].Messages[j].Content)\n        }\n    }\n}","typeGuard":"func isStringOrNullJSON(raw []byte) bool {\n    var s string\n    return json.Unmarshal(raw, &s) == nil || bytes.Equal(bytes.TrimSpace(raw), []byte(\"null\"))\n}","tryCatchPattern":"if _, err := cachebench.BuildCorpusTrace(p, corpus); err != nil {\n    if err.Error() == \"corpus provider conversion supports string or null content\" {\n        // normalize content blocks to joined text, rebuild trace\n    }\n}","preventionTips":["Flatten OpenAI multi-part content arrays into a single joined string at corpus ingest","Enforce string-or-null content in your own corpus validator so failures surface at load time","Document that corpora must be schema-normalized before provider-native trace building"],"tags":["go","corpus","provider-conversion","json"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}