oobabooga/textgen · error · RuntimeError

Dataset row must contain "messages" or "conversations" key.

Error message

Dataset row must contain "messages" or "conversations" key. Found: {list(data_point.keys())}

What it means

Thrown by normalize_messages() during LoRA/dataset training when a dataset row contains neither a 'messages' key (OpenAI chat format) nor a 'conversations' key (ShareGPT format). The training tokenizer path requires one of these schemas to build chat-template input, so any other column layout aborts preprocessing. The message lists the actual keys found, which tells you exactly what schema the row does have.

Source

Thrown at modules/training.py:343

        target_mods = [f"{name}_proj" for name, enabled in {
            "q": q_proj_en, "k": k_proj_en, "v": v_proj_en, "o": o_proj_en,
            "gate": gate_proj_en, "down": down_proj_en, "up": up_proj_en,
        }.items() if enabled]
        return target_mods

    def normalize_messages(data_point):
        """Convert a dataset row to OpenAI messages format for apply_chat_template()."""
        if "messages" in data_point:
            return data_point["messages"]

        if "conversations" in data_point:
            role_map = {"human": "user", "gpt": "assistant"}
            return [
                {"role": role_map.get(turn.get("from", ""), turn.get("from", "")), "content": turn["value"]}
                for turn in data_point["conversations"]
            ]

        raise RuntimeError(
            f'Dataset row must contain "messages" or "conversations" key. '
            f'Found: {list(data_point.keys())}'
        )

    def tokenize_conversation(data_point):
        """Tokenize using apply_chat_template() with assistant-only label masking."""
        messages = normalize_messages(data_point)
        full_ids = list(shared.tokenizer.apply_chat_template(messages, tokenize=True, return_dict=False))

        # Build labels: -100 for everything, then unmask assistant turns.
        # This assumes apply_chat_template(messages[:i]) is a token-for-token
        # prefix of apply_chat_template(messages[:i+1]), which holds for all
        # standard chat templates (Llama, ChatML, Mistral, etc.).
        labels = [-100] * len(full_ids)
        for i, msg in enumerate(messages):
            if msg["role"] == "assistant":
                # Tokens up to where this assistant turn starts
                header_ids = shared.tokenizer.apply_chat_template(

View on GitHub (pinned to ed888c71f2)

Solutions

  1. Convert the dataset to one of the accepted schemas: either [{'role': 'user'|'assistant'|'system', 'content': str}] under a 'messages' key, or [{'from': 'human'|'gpt', 'value': str}] under a 'conversations' key.
  2. If the source is Alpaca-style, map instruction/output columns to messages programmatically before training (e.g. df.apply to build the messages list).
  3. Check for typos or nested structures in the dataset column names by printing dataset[0].keys() and comparing against the two accepted keys.
  4. As a last resort, pre-transform the dataset file offline (jq/pandas) so every row literally has 'messages' or 'conversations' at the top level.

Example fix

# before (row only has Alpaca columns)
# {"instruction": "Hi", "output": "Hello!"}  -> RuntimeError

# after: convert to messages format
import json
rows = [json.loads(l) for l in open('data.jsonl')]
with open('data_fixed.jsonl', 'w') as f:
    for r in rows:
        f.write(json.dumps({"messages": [
            {"role": "user", "content": r["instruction"]},
            {"role": "assistant", "content": r["output"]},
        ]}) + '\n')
Defensive patterns

Strategy: validation

Validate before calling

REQUIRED_KEYS = {"messages", "conversations"}

def row_is_trainable(row: dict) -> bool:
    return bool(REQUIRED_KEYS & set(row.keys()))

# before mapping the dataset:
bad = [i for i, row in enumerate(dataset) if not row_is_trainable(row)]
if bad:
    raise ValueError(f'{len(bad)} rows lack messages/conversations; first bad index: {bad[0]}, '
                     f'keys: {list(dataset[bad[0]].keys())}')

Type guard

from typing import TypedDict

class Message(TypedDict):
    role: str
    content: str

class MessagesRow(TypedDict):
    messages: list[Message]

class Turn(TypedDict):
    from_: str  # 'human' | 'gpt'
    value: str

class ConversationsRow(TypedDict):
    conversations: list[Turn]

def is_chat_row(row: dict) -> bool:
    if "messages" in row:
        msgs = row["messages"]
        return isinstance(msgs, list) and all(
            isinstance(m, dict) and m.get("role") in {"user", "assistant", "system"} and isinstance(m.get("content"), str)
            for m in msgs
        )
    if "conversations" in row:
        turns = row["conversations"]
        return isinstance(turns, list) and all(
            isinstance(t, dict) and t.get("from") in {"human", "gpt", "system"} and isinstance(t.get("value"), str)
            for t in turns
        )
    return False

Prevention

When it happens

Trigger: Loading a Hugging Face dataset whose rows use a different schema (e.g. only 'text', 'prompt'/'response', 'instruction'/'input'/'output' Alpaca-style columns) and starting training with any apply_chat_template-based dataset path. Also triggered by datasets that nest the chat under another column name, or by a mapping step that was skipped.

Common situations: User points --dataset at an Alpaca-format JSON or a raw completion corpus while the trainer expects chat-format data; dataset was regenerated with renamed columns; a ShareGPT export uses 'from'/'value' but the top-level key is 'chat' or 'history' instead of 'conversations'.


AI-assisted analysis of oobabooga/textgen@ed888c71f2 (2026-08-15). Data as JSON: /api/errors/ab73ec63fb3b7bc0. Report an issue: GitHub.