Most fine-tuning projects fail because of bad data, not bad models. In this tutorial I will show you how to build a synthetic dataset generator that produces diverse, high-quality instruction-response pairs and filters them for quality before exporting to JSONL. The pipeline runs entirely on Oxlo.ai, so long system prompts and multi-step rollouts do not inflate your bill the way token-based pricing would.
What you'll need
- Python 3.10+
- The OpenAI SDK:
pip install openai - An Oxlo.ai API key from https://portal.oxlo.ai
- A local directory to write the output
Step 1: Configure the client
I use qwen-3-32b as the generator because it handles multilingual reasoning and agentic instructions well. Initialize the client exactly like the OpenAI SDK, but point it at Oxlo.ai.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello"},
],
)
print(response.choices[0].message.content)
Step 2: Define the dataset schema
We need the model to emit strict JSON. The system prompt below forces a single valid JSON object with instruction, input, output, and category fields. I keep the categories broad so the resulting dataset generalizes.
SYSTEM_PROMPT = """You are a synthetic data engineer. Given a topic, generate 5 diverse instruction-response pairs for fine-tuning a helpful assistant.
Rules:
- Output must be a single JSON object.
- The top-level key is "items" containing a list of 5 objects.
- Each object has keys: "instruction" (string), "input" (string, optional context), "output" (string), "category" (string).
- Instructions should be varied: some open-ended, some with specific constraints, some requiring reasoning.
- Outputs must be factually accurate and concise.
Example format:
{
"items": [
{
"instruction": "Explain the concept of...",
"input": "",
"output": "...",
"category": "science"
}
]
}
Respond with only the JSON object. Do not wrap it in markdown code fences."""
Step 3: Generate seed topics
Instead of hard-coding topics, I ask the model to produce them. This keeps the distribution fresh and reduces human bias.
import json
seed_prompt = "Generate a JSON array of 20 diverse, specific topics suitable for creating training data for a general-purpose assistant. Examples: 'PyTorch memory management', 'medieval irrigation systems', 'contract law basics'. Output only the JSON array."
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": "You emit only valid JSON arrays."},
{"role": "user", "content": seed_prompt},
],
)
raw = response.choices[0].message.content
topics = json.loads(raw)
print(f"Generated {len(topics)} topics")
print(topics[:3])
Step 4: Generate instruction pairs
Now we loop over the seeds. Because Oxlo.ai uses request-based pricing, running 20 requests with long system prompts costs the same as 20 short requests. On a token-based provider, those repeated long prompts would multiply cost. See https://oxlo.ai/pricing for plan details.
dataset = []
for topic in topics:
user_message = f'Generate instruction-response pairs for the topic: "{topic}"'
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
)
raw = response.choices[0].message.content.strip()
if raw.startswith("
```"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
batch = json.loads(raw)
dataset.extend(batch["items"])
print(f"Topic '{topic}' yielded {len(batch['items'])} items")
print(f"Total raw items: {len(dataset)}")
Step 5: Filter for quality
Raw synthetic data is noisy. I run a judge pass with kimi-k2.6, which scores each entry on a 1 to 5 scale for accuracy and helpfulness. We keep only 4s and 5s.
JUDGE_PROMPT = """Rate the following training example on a scale of 1 to 5, where 5 is excellent.
Consider accuracy, clarity, and helpfulness. Respond with only a JSON object: {"score": int, "reason": string}.
Instruction: {{instruction}}
Input: {{input}}
Output: {{output}}"""
filtered = []
for item in dataset:
user_message = JUDGE_PROMPT.replace("{{instruction}}", item["instruction"]).replace("{{input}}", item["input"]).replace("{{output}}", item["output"])
response = client.chat.completions.create(
model="kimi-k2.6",
messages=[
{"role": "system", "content": "You are a strict quality judge. Respond only with the requested JSON."},
{"role": "user", "content": user_message},
],
)
raw = response.choices[0].message.content.strip()
if raw.startswith("
```"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
result = json.loads(raw)
if result["score"] >= 4:
item["quality_score"] = result["score"]
item["quality_reason"] = result["reason"]
filtered.append(item)
print(f"Retained {len(filtered)} of {len(dataset)} items")
Step 6: Export to JSONL
Trainers expect JSONL with a messages array or prompt-completion fields. I format each record as a messages list to match the ChatML style used by Oxlo.ai and most modern frameworks.
import os
os.makedirs("out", exist_ok=True)
out_path = "out/training_data.jsonl"
with open(out_path, "w", encoding="utf-8") as f:
for item in filtered:
record = {
"messages": [
{"role": "user", "content": item["instruction"] + "\n" + item["input"]},
{"role": "assistant", "content": item["output"]}
],
"metadata": {
"category": item["category"],
"quality_score": item.get("quality_score"),
"quality_reason": item.get("quality_reason")
}
}
f.write(json.dumps(record, ensure_ascii=False) + "\n")
print(f"Wrote {len(filtered)} records to {out_path}")
Run it
Tie the steps together in a single script and execute. Here is the full main flow.
if __name__ == "__main__":
# Ensure the client is initialized
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
# Step 3: seeds
seed_prompt = "Generate a JSON array of 20 diverse, specific topics suitable for creating training data for a general-purpose assistant. Output only the JSON array."
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": "You emit only valid JSON arrays."},
{"role": "user", "content": seed_prompt},
],
)
topics = json.loads(response.choices[0].message.content)
# Step 4: generate
dataset = []
for topic in topics:
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f'Generate instruction-response pairs for the topic: "{topic}"'},
],
)
raw = response.choices[0].message.content.strip()
if raw.startswith("
```"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
batch = json.loads(raw)
dataset.extend(batch["items"])
# Step 5: judge
filtered = []
for item in dataset:
user_message = JUDGE_PROMPT.replace("{{instruction}}", item["instruction"]).replace("{{input}}", item["input"]).replace("{{output}}", item["output"])
response = client.chat.completions.create(
model="kimi-k2.6",
messages=[
{"role": "system", "content": "You are a strict quality judge. Respond only with the requested JSON."},
{"role": "user", "content": user_message},
],
)
raw = response.choices[0].message.content.strip()
if raw.startswith("
```"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
result = json.loads(raw)
if result["score"] >= 4:
item["quality_score"] = result["score"]
item["quality_reason"] = result["reason"]
filtered.append(item)
# Step 6: export
os.makedirs("out", exist_ok=True)
with open("out/training_data.jsonl", "w", encoding="utf-8") as f:
for item in filtered:
record = {
"messages": [
{"role": "user", "content": item["instruction"] + "\n" + item["input"]},
{"role": "assistant", "content": item["output"]}
],
"metadata": {
"category": item["category"],
"quality_score": item.get("quality_score"),
"quality_reason": item.get("quality_reason")
}
}
f.write(json.dumps(record, ensure_ascii=False) + "\n")
print(f"Finished. Generated {len(dataset)} raw items, retained {len(filtered)}.")
print("Preview:")
print(json.dumps(filtered[0], indent=2, ensure_ascii=False))
Example output when I ran this:
Finished. Generated 100 raw items, retained 78.
Preview:
{
"instruction": "Summarize the key differences between TCP and UDP",
"input": "",
"output": "TCP is connection-oriented, reliable, and ordered...",
"category": "networking",
"quality_score": 5,
"quality_reason": "Accurate, concise, and directly addresses the instruction."
}
Wrap-up
Swap qwen-3-32b for deepseek-v4-flash if you need to distill from a 1M context source document, or switch the judge to deepseek-v3.2 when you need strong coding and reasoning validation. If you are generating thousands of rows, Oxlo.ai request-based pricing stays flat regardless of prompt length, which makes large-scale synthetic dataset construction far more predictable than token-based alternatives.
Two concrete next steps. First, feed the resulting JSONL into a fine-tuning run, either locally with axolotl or via an Oxlo.ai Enterprise dedicated GPU plan. Second, adapt the system prompt to a narrow domain, such as legal contract review or medical coding, and use the same pipeline to bootstrap a specialized model.
Top comments (0)