RAG and data · 2026-10-17
Categorizing a product catalog with d1-ultra, with review
Categorize your product catalog with liquid/d1-ultra: a two-level category tree, example thresholds, a review queue and batch processing in Python.
Short answer: ask category questions per product, send the uncertain ones to review
LLMTR (llmtr.com) is a gateway that gives access to language models from different providers through one OpenAI-compatible API. To categorize your product catalog, you call d1-ultra, Liquid AI's decision model, there with the model id liquid/d1-ultra on the POST /v1/systemone endpoint; the base address is https://llmtr.com/v1. The model card describes it as a model for routing, classification and image checks: for every question it returns a typed answer with probabilities, not text.
The method has four parts: a category tree written with option descriptions, a choice question for each level of the tree, thresholds in your code, and a review queue. A product whose confidence is above the threshold gets its category automatically. A product below the threshold, one where other was chosen, or one whose request failed is set aside for a person to review. Uncertain products therefore never go quietly into the wrong category; they land in front of someone.
How do you categorize a product catalog with a decision model?
First write your category tree: a few departments at the top level and categories under each department. Give every option a short description and add an other option with a null value for products that fit none of them. Send the product's title and description in the state field as JSON. In the first request, ask for the top-level department with a choice question, and ask with a noul question whether the product is a set made of several different products. If the department's confidence passes the threshold, ask for that department's categories with a new choice question in a second request.
The category is asked in a separate request because questions in one request are evaluated independently: the second question cannot see the first answer, and which sub-list you ask about depends on that first answer. Each of the two levels has its own threshold. When either level falls below its threshold, when other is chosen, or when the set probability is high, the product goes to the review queue.
The category tree: one list or two levels?
If you have only a few categories, you can offer every leaf category as an option in a single choice question; each product then gets one question. Because d1-ultra reads and bills every question as a separate prompt along with the state, a single list uses the fewest tokens. As the list grows, more options sit close to each other, the probability splits between them and the confidence drops. At that point a two-level tree offers a shorter list at each level and lets you tune the threshold per level.
Option descriptions should draw the category's boundary: say briefly which categories a department covers and which products belong in a category, in words that do not overlap. When two options cover the same product, the model ends up between them; that is the tree's own ambiguity, and it shows up in the review queue. When you change the tree, check the thresholds again.
Batch processing: running the whole catalog through a script
The Python script below processes every product in products.csv (sku, title and description columns) in turn. For each product it sends the department and set questions first and, if the threshold is passed, the category question. Products whose category is settled are written to categorized.csv with a department/category path, and the others to review.csv.
A network error, an HTTP error or a response in an unexpected shape sends the product to the review queue, so no product is lost. If the key's request limit is exceeded you get 429 rate_limit_error; the script waits for the Retry-After header if there is one, five seconds otherwise, and tries the same request at most three times. If your credit balance does not cover the request you get 402 insufficient_credits and the script stops, because otherwise every remaining product would fall into review. If the model cannot answer for a while you get 503 model_unavailable and the product goes to review. For large catalogs, extend the script to write results product by product, and store each result with the question text and threshold you used.
A Python requests script that categorizes the products in products.csv with a two-level tree and writes the uncertain ones to review.csv.
import csv
import os
import time
import requests
API_KEY = os.environ["LLMTR_API_KEY"]
TOP_THRESHOLD = 0.8 # example value: tune it on products your team has already categorized
SUB_THRESHOLD = 0.7 # example value
BUNDLE_THRESHOLD = 0.5 # example value
DEPARTMENTS = {
"home": "Cookware, storage and lighting for the home",
"outdoor": "Garden tools, camping gear and grills",
"sports": "Fitness, cycling and team sports equipment",
"other": None,
}
CATEGORIES = {
"home": {"cookware": "Pots, pans, baking trays", "storage": "Boxes, jars, shelves", "lighting": "Lamps and bulbs", "other": None},
"outdoor": {"garden_tools": "Shovels, pruners, hoses", "camping": "Tents, sleeping bags, camp cookware", "grills": "Barbecues and grill accessories", "other": None},
"sports": {"fitness": "Weights, mats, bands", "cycling": "Bikes, helmets, bike parts", "team_sports": "Balls, nets, goals", "other": None},
}
def ask(state, questions):
for _ in range(3):
response = requests.post(
"https://llmtr.com/v1/systemone",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"model": "liquid/d1-ultra", "state": state, "questions": questions},
timeout=60,
)
if response.status_code == 402:
raise SystemExit("Not enough credit: top up your balance and run again.")
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", "5")))
continue
response.raise_for_status()
return response.json()["answers"]
raise RuntimeError("still rate limited")
def categorize(product):
state = {"title": product["title"], "description": product["description"]}
first = ask(state, {
"department": {"type": "choice", "instructions": "Which department does this product belong to?", "criteria": DEPARTMENTS},
"bundle": {"type": "noul", "instructions": "Does the listing sell several different products as one set?"},
})
department = first["department"]
if (department["choice"] == "other" or department["confidence"] < TOP_THRESHOLD
or first["bundle"]["noul"] >= BUNDLE_THRESHOLD):
return None
second = ask(state, {
"category": {
"type": "choice",
"instructions": f"Which {department['choice']} category does this product belong to?",
"criteria": CATEGORIES[department["choice"]],
},
})
category = second["category"]
if category["choice"] == "other" or category["confidence"] < SUB_THRESHOLD:
return None
return department["choice"] + "/" + category["choice"]
categorized, review = [], []
with open("products.csv", newline="", encoding="utf-8") as f:
for product in csv.DictReader(f): # columns: sku, title, description
try:
path = categorize(product)
except (requests.RequestException, RuntimeError, KeyError, TypeError, ValueError):
path = None
if path:
categorized.append({"sku": product["sku"], "category": path})
else:
review.append({"sku": product["sku"], "title": product["title"]})
for name, rows, fields in [("categorized.csv", categorized, ["sku", "category"]), ("review.csv", review, ["sku", "title"])]:
with open(name, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
print(len(categorized), "categorized,", len(review), "sent to review")
Choosing thresholds and running the review queue
The example response below shows the shape of the first request; the numbers are examples. A cast iron skillet that works over a campfire and in a home oven fits both the home and the outdoor department; the probability splits between the two options, and with a confidence of 0.33 it stays below the 0.8 threshold, so the product goes to the review queue. The bundle answer is the probability that the product is a set.
The thresholds of 0.8, 0.7 and 0.5 in the code are examples; LLMTR does not suggest a default threshold, and the thresholds in Liquid's documentation are examples too. To choose them, run a few hundred products your team has already categorized through the script, and look at how many products are categorized automatically at each threshold value and how many of those are right. The categories a person assigns in the review queue grow this labelled set over time; check the tree and the thresholds against it again. Raise the threshold for departments where a wrong category is costly.
| Answer | Action |
|---|---|
| department.confidence 0.8 or higher, choice is not other, bundle.noul below 0.5 | the category question is sent |
| department.confidence below 0.8, choice is other, or bundle.noul 0.5 or higher | product goes to the review queue |
| category.confidence 0.7 or higher, choice is not other | product is saved with its department/category path |
| category.confidence below 0.7, or choice is other | product goes to the review queue |
| HTTP error or no response | product goes to the review queue |
| 402 insufficient_credits | the script stops; top up your balance and run it again |
Example response: the fields follow Liquid's documented shape, the numbers are examples.
{
"model": "liquid/d1-ultra",
"answers": {
"department": {
"type": "choice",
"choice": "outdoor",
"probabilities": { "home": 0.45, "outdoor": 0.5, "sports": 0.03, "other": 0.02 },
"confidence": 0.33
},
"bundle": { "type": "noul", "noul": 0.04 }
},
"usage": { "input_tokens": 604, "output_tokens": 0 }
}
Cost, limits and the data notice
For liquid/d1-ultra, 1M input tokens cost $0.042 and output costs $0. According to Liquid's announcement, each question is billed as its own prompt; LLMTR charges from the usage.input_tokens value in the response. A worked example: in a catalog of 20,000 products where each product gets at most three questions (department, set and category) and each question, state included, is about 300 tokens, the upper bound is 20,000 x 3 x 300 = 18,000,000 tokens, that is 18,000,000 x $0.042 / 1,000,000 = $0.756. Products set aside for review at the first level never get the category question, so the real amount stays below that.
Each question, along with the state, cannot exceed 32,768 tokens; a request over that returns 422 provider_error, so for long descriptions send the part that matters for the category. You can add a product photo with the images array (JPEG, PNG or WebP, at most 8); the image is counted again for every question, and a 1024 x 1024 image costs 1,536 tokens per question. The data notice on the model card is explicit: this model is served through a third-party provider, and the text and images you send may be retained by the provider and used to train Liquid's models. Do not send sensitive, personal or customer data: put the product title and description in the state field, not customer reviews or seller contact details. Liquid does not publish a list of supported languages for d1-ultra; add Turkish descriptions to your labelled set as well.
| Property | Value |
|---|---|
| Model id | liquid/d1-ultra |
| Endpoint | POST /v1/systemone |
| Input price | $0.042 per 1M tokens |
| Output price | $0 per 1M tokens |
| Billing | every question is counted with the state |
| Limit per question | 32,768 tokens (state and images included) |
| Product images | up to 8 per request; JPEG, PNG, WebP |
| Most questions per request | 1,000 questions |
Categorize a product catalog with d1-ultra
Write the category tree, choose the thresholds with labelled products and process the catalog with a script that sends uncertain products to a review queue.
- Category tree. Write the departments and categories with short descriptions in the criteria field of choice questions, and add an other option with a null value at each level.
- Thresholds. Send a few hundred already categorized products to https://llmtr.com/v1/systemone with liquid/d1-ultra and choose each level's confidence threshold by looking at the share of correct categories.
- Batch run. Run the catalog through the script; write products that pass to the category file, and products below the threshold, products where other was chosen and products whose request failed to the review queue.
Frequently asked questions
How many categories can I give in one choice question?
There is no documented option limit for d1-ultra; LLMTR accepts at most 1,000 questions per request. As the list grows, the probability splits between similar options and the confidence drops; when the number of categories grows, a two-level tree offers a shorter list at each level.
Which threshold value should I use?
There is no default threshold; the 0.8, 0.7 and 0.5 in this article are examples. Send products your team has already categorized to the model and choose by looking at the share of products categorized automatically at each threshold value and how many of them are right.
Can I send the product photo too?
Yes. Send it in the top-level images array, as a base64 data URL or as an object with content_type and base64 fields. JPEG, PNG and WebP are accepted, a request can carry up to 8 images, and each image is counted again for every question.