JuliusBrussee/caveman · error

corpus provider conversion supports string or null content

Error message

corpus provider conversion supports string or null content

What it means

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.

Source

Thrown at cacheengine/cachebench/corpus.go:734

		return anthropicCorpusBody(provider.Model, messages)
	case "bedrock":
		return bedrockCorpusBody(messages)
	case "gemini":
		return geminiCorpusBody(messages)
	default:
		return nil, fmt.Errorf("unsupported corpus provider %q", provider.Provider)
	}
}

func contentText(content json.RawMessage) (string, error) {
	if len(content) == 0 || bytes.Equal(bytes.TrimSpace(content), []byte("null")) {
		return "", nil
	}
	var text string
	if err := json.Unmarshal(content, &text); err == nil {
		return text, nil
	}
	return "", errors.New("corpus provider conversion supports string or null content")
}

func anthropicCorpusBody(model string, messages []CorpusMessage) ([]byte, error) {
	var system []string
	converted := make([]any, 0, len(messages))
	for _, message := range messages {
		text, err := contentText(message.Content)
		if err != nil {
			return nil, err
		}
		switch message.Role {
		case "system", "developer":
			if text != "" {
				system = append(system, text)
			}
		case "assistant":
			blocks := make([]any, 0, len(message.ToolCalls)+1)
			if text != "" {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Normalize content to a string when building the corpus: join text parts of content arrays into one string (or take the first text part)
  2. Null non-text parts (image_url etc.) or drop those messages if text-only replay is unacceptable
  3. Enforce string-or-null content at corpus validation time so the failure happens at load, not provider conversion

Example fix

// before
msg.Content = json.RawMessage(`[{"type":"text","text":"hello"}]`) // triggers error

// after
text := extractTextParts(msg.Content) // "hello"
msg.Content = json.RawMessage(strconv.Quote(text))
Defensive patterns

Strategy: validation

Validate before calling

func isTextOrNullContent(raw json.RawMessage) bool {
    t := bytes.TrimSpace(raw)
    if len(t) == 0 || bytes.Equal(t, []byte("null")) { return true }
    var s string
    return json.Unmarshal(t, &s) == nil
}

for i := range corpus.Rows {
    for j := range corpus.Rows[i].Messages {
        if !isTextOrNullContent(corpus.Rows[i].Messages[j].Content) {
            corpus.Rows[i].Messages[j].Content = normalizeToText(corpus.Rows[i].Messages[j].Content)
        }
    }
}

Type guard

func isStringOrNullJSON(raw []byte) bool {
    var s string
    return json.Unmarshal(raw, &s) == nil || bytes.Equal(bytes.TrimSpace(raw), []byte("null"))
}

Try / catch

if _, err := cachebench.BuildCorpusTrace(p, corpus); err != nil {
    if err.Error() == "corpus provider conversion supports string or null content" {
        // normalize content blocks to joined text, rebuild trace
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/6470de1a57733230. Report an issue: GitHub.