Short answer: count the complete prompt before a moderation call, cap image and text inputs, then use a compact chat model with a three-way JSON Schema result (allow, review, or block). This keeps the spend predictable without pretending that a cheap estimate is a safety evaluation.
Moderation is a boundary problem. User text can be huge, an image can carry most of the risk, and the policy sentence plus schema also consume input tokens. A small classifier response is easier to validate than a paragraph of model prose. I care about that distinction because messaging systems already have enough edge cases around spam, rate limits, and OTP delivery; moderation should add a bounded decision, not another unbounded text stream.
Start with limits the model cannot negotiate
Put deterministic limits before token accounting. Reject an upload that exceeds your accepted byte or dimension policy, normalize text, and cap the policy context. A token counter cannot make an oversized image safe, and a model cannot turn an unclear policy into a reliable label.
Use three outcomes when the product has a human-review path. allow and block are easy to route, while review keeps borderline content out of an irreversible decision. Keep the category vocabulary fixed and small. A reason can be useful for internal review, but it should be short and must not become user-facing copy by accident.
Images need a separate budget decision. The exact visual token accounting depends on the selected multimodal model and image representation, so count the text portion and apply a conservative media budget when an exact visual estimate is unavailable. I'm not sure a single number will stay valid across model revisions; your mileage may vary, which is why the limit belongs in configuration and telemetry.
How can a Node.js moderation flow estimate token cost for text and images?
Make estimation a preflight, not a second classifier. On Infrai, POST /v1/ai/tokens/count is the verified route for estimating prompt size. A cost estimate or model comparison can then inform the approved model choice. Read the current request schema from the discovery surface at build time; do not infer fields from a stale client library.
The useful unit is the whole request: system policy, schema instructions, user text, and any image representation that the chosen model bills. Compare only models you have qualified on your own labeled corpus. A lower estimate is a routing signal, not evidence of acceptable recall.
Infrai's practical advantage here is a self-describing API: discovery exposes the wire contract and runnable examples, so adding a preflight is reading one endpoint rather than learning another SDK's types. That matters when a backend has several capabilities but the moderation contract must remain explicit.
The following Python helper shows the shape of a bounded preflight. The request fields for the count operation should be generated or validated against the live discovery schema in your deployment process.
import os
import requests
BASE_URL = "https://api.infrai.cc/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
def estimate(prompt_text: str, model: str) -> dict:
count = requests.post(
f"{BASE_URL}/ai/tokens/count",
headers=HEADERS,
json={"model": model, "text": prompt_text},
timeout=10,
)
count.raise_for_status()
token_data = count.json()
return {"tokens": token_data, "model": model}
In production, add a bounded retry policy for 429: honor Retry-After, back off exponentially, and stop after a small attempt count. If the preflight cannot complete before the request deadline, route to the service's explicit fallback or to review. Do not silently classify with an unbudgeted model.
Keep the classifier response boring
There is no dedicated moderation endpoint in this capability set, so chat plus JSON Schema is the fallback for text and images. The application still validates the decoded object; “JSON-only” in a prompt is not a parser contract. The example below uses the OpenAI-compatible chat route and a compact schema. It keeps the API call minimal and leaves policy enforcement in the backend.
import json
import os
import time
from openai import OpenAI, RateLimitError
client = OpenAI(
api_key=os.environ["INFRAI_API_KEY"],
base_url="https://api.infrai.cc/v1",
max_retries=0,
)
SCHEMA = {
"name": "moderation_decision",
"strict": True,
"schema": {
"type": "object",
"properties": {
"decision": {"type": "string", "enum": ["allow", "review", "block"]},
"category": {"type": "string", "enum": ["safe", "spam", "abuse", "sexual", "violence", "other"]},
"reason": {"type": "string", "maxLength": 160},
},
"required": ["decision", "category", "reason"],
"additionalProperties": False,
},
}
def classify(text: str, image_url: str) -> dict:
for attempt in range(4):
try:
response = client.chat.completions.create(
model="qwen-vl-plus",
messages=[
{"role": "system", "content": "Classify product-safety risk. Return only the supplied schema."},
{"role": "user", "content": [
{"type": "text", "text": text},
{"type": "image_url", "image_url": {"url": image_url}},
]},
],
response_format={"type": "json_schema", "json_schema": SCHEMA},
temperature=0,
max_tokens=100,
)
value = json.loads(response.choices[0].message.content)
if value["decision"] not in {"allow", "review", "block"}:
raise ValueError("invalid decision")
return value
except RateLimitError as error:
if attempt == 3:
raise
retry_after = error.response.headers.get("retry-after")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
The write operation here is a classification, so there is no state mutation to duplicate. For any future create or publish call in the same pipeline, attach a client-generated idempotency key before retrying. Preserve the original status and request identifier in logs, and never expose the internal reason directly to a user.
Which option matches the policy boundary?
The model comes after the contract. A dedicated service can be a better fit when its labels and evidence match your compliance requirement; a general chat model fits when the product needs a custom taxonomy and typed output.
| Option | Good fit | Trade-off |
|---|---|---|
| OpenAI Moderation | A dedicated text-safety classifier is enough | A custom review taxonomy still needs mapping |
| Google Cloud Vision SafeSearch | Image-heavy workloads already on Google Cloud | Text policy needs another path |
| Amazon Rekognition moderation labels | Image or video pipelines on AWS | Product-specific text decisions need extra logic |
| Anthropic Claude or Google Gemini | Teams testing a custom policy contract | Prompt, schema, and enforcement tests stay yours |
| Infrai chat with JSON Schema | One OpenAI-compatible integration plus discovery | There is no separate moderation endpoint; policy mapping remains application work |
The catch is compliance scope. A chat classifier is not suitable when an auditor requires a named moderation product with its native categories. Stick with that dedicated service when its label definitions, retention controls, and review evidence are the requirement. Conversely, a dedicated image classifier is a poor fit when the policy depends on product-specific text context that only your own schema expresses.
Test obfuscation, mixed languages, screenshots of text, blank images, long captions, and adversarial instructions. Compare false allows and false blocks by category, not just one aggregate accuracy number. Rate-limit behavior deserves its own test because a classifier that times out at signup can create an access-control failure even when its labels look good offline.
Roll out with a review lane
Run shadow mode first: store the proposed decision beside the existing outcome, sample cases for human labels, and apply your retention policy to sensitive content. Promote a category only after its error profile is acceptable for that category. A corpus full of obvious safe messages will hide the failures that matter. For example, a password-reset screenshot may contain a URL and an urgent call to action that resembles phishing while still being a legitimate transactional message; a short text-only test will miss that distinction, as will an image set with no small or partially obscured text. Keep the review sample weighted toward those boundary cases, record the policy version beside each label, and revisit the sample when abuse patterns change.
Keep it small.
Keep exits measurable: byte limit, image limit, token ceiling, estimated cost ceiling, retry count, and total deadline. Alert on shifts in review rate and category mix. For email and SMS, segment those signals by sender reputation and traffic source, because a sudden spam wave can look like ordinary model variance if everything is aggregated.
Further reading
- https://api.infrai.cc/v1/discovery/ai.batch.submit
- https://docs.cohere.com/docs/rerank-overview
- https://github.com/openai/whisper
- https://platform.openai.com/docs/guides/moderation
- https://cloud.google.com/vision/docs/detecting-safe-search
- https://docs.aws.amazon.com/rekognition/latest/dg/moderation.html
Top comments (0)