TL;DR: For an edtech system scoring candidates against a job rubric, use a dedicated moderation API when it exists and its taxonomy fits. When no native moderation endpoint exists, a cheap chat model with strict JSON Schema is the practical fallback. The deciding constraint is structured correctness: every response must be parsed, validated, retried, or escalated without turning a safety decision into prose.
I would accept a candidate only after the moderation result passes schema validation. An invalid result is a review-queue event, not a safe result. That rule makes the architecture easier to reason about than a clever prompt.
What should the moderation boundary guarantee?
The concrete job is familiar: an edtech product receives learner-submitted text, then scores a candidate against a job rubric. The text may be shown to a recruiter, stored for an audit, and fed into later scoring. A moderation response therefore needs a stable contract. My smallest useful contract is {"is_safe": boolean, "reason": string}.
There are four invariants:
- The model receives only the text and policy context it needs.
- The response validates against the expected JSON Schema.
- A timeout, 429, malformed response, or provider error never becomes
is_safe: true. - A human review path exists for ambiguous policy cases.
That last point matters in education. A profanity classifier and a policy decision are different things. A name, disability disclosure, political opinion, or quoted classroom material can be sensitive without being disallowed. The model can classify; policy code owns the final action.
The tempting assumption is that valid JSON means a valid decision. It does not.
OpenAI moderation or JSON-schema chat: which boundary fits?
A dedicated moderation endpoint is cleaner when it exists, is stable, and has a taxonomy aligned with your policy. It gives the application a narrower operational surface and avoids spending prompt space explaining what “safe” means.
The constraint in this system is that no native moderation endpoint is available. The fallback is a standard chat completion with strict json_schema. GPT-4o-mini is a reasonable choice for short text because the task is small and the output contract is explicit. This is a workaround, but a disciplined one: the schema is part of the interface, not a hopeful instruction at the end of a prompt.
| Option | Strength | Boundary | Good fit here? |
|---|---|---|---|
| OpenAI Moderation API | Purpose-built categories and a narrow contract | Requires a dedicated moderation surface and its taxonomy | Yes when available |
| GPT-4o-mini with JSON Schema | Returns the exact fields the workflow consumes | Prompt and policy wording become part of the safety design | Yes when a native endpoint is unavailable |
| Anthropic Claude | General-purpose text reasoning and configurable instructions | General chat output still needs a schema gate and policy tests | Useful for nuanced review |
| Google Gemini | Multimodal chat can cover text and image inputs | Safety settings and structured output need provider-specific tests | Useful for multimodal workflows |
The table is a decision aid, not a leaderboard. OpenAI’s dedicated moderation surface is preferable for a narrow moderation job. Claude and Gemini are credible alternatives for policy-heavy or multimodal workflows, but neither removes the need to test borderline examples. The chat fallback wins only on fit: it is available through the existing compatible chat surface and returns the shape the scoring pipeline already understands.
For short inputs, chat moderation is cheap enough to substitute for a missing dedicated endpoint. I would still measure volume, retry rate, and review rate before committing to a budget. A unit price without those three numbers is theater.
The tradeoff is blunt: you own more policy testing because the model is a general chat model.
That is acceptable.
The critical path is a schema gate, not a prompt
This is the smallest production-shaped path I would start with. It uses the OpenAI Python client against the compatible base URL, reads the key from the environment, retries 429 responses with exponential backoff, honors Retry-After when the SDK exposes it, and rejects anything that does not validate.
import json
import os
import time
from openai import OpenAI, RateLimitError
client = OpenAI(
api_key=os.environ["INFRAI_API_KEY"],
base_url=os.environ["AI_BASE_URL"],
)
result_schema = {
"type": "object",
"properties": {
"is_safe": {"type": "boolean"},
"reason": {"type": "string"},
},
"required": ["is_safe", "reason"],
"additionalProperties": False,
}
def moderate(text):
for attempt in range(4):
try:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"Classify user content for an edtech candidate-scoring workflow. "
"Return unsafe for threats, targeted harassment, sexual exploitation, "
"or instructions for wrongdoing. Return only the requested object."
),
},
{"role": "user", "content": text},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "moderation_result",
"strict": True,
"schema": result_schema,
},
},
)
value = json.loads(response.choices[0].message.content)
if (
isinstance(value, dict)
and isinstance(value.get("is_safe"), bool)
and isinstance(value.get("reason"), str)
):
return value
raise ValueError("moderation response failed schema validation")
except RateLimitError as error:
if attempt == 3:
raise
headers = getattr(getattr(error, "response", None), "headers", {})
retry_after = headers.get("retry-after")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
raise RuntimeError("moderation did not return a result")
The code deliberately does not catch every exception and quietly mark content safe. A 4xx response should remain an error with its provider message; malformed JSON should enter review or fail closed according to product policy. In a queue, attach a stable content identifier to the job and make the consumer idempotent. At-least-once delivery is normal; duplicate moderation must not duplicate a candidate decision or audit record.
The discovery surface is useful because it is self-describing: discovery can expose a capability’s request schema, response schema, billing metadata, and runnable examples without requiring a new SDK lesson. That reduces integration friction, but it does not validate policy. The schema gate and test corpus still do that work. In the review-and-audit path, this matters after the first prototype: an engineer can inspect the response contract before wiring the result into a private object, a recruiter-facing queue, and a retrying worker, rather than reverse-engineering three SDKs. The unified surface also means one key and one bill cover the related backend capabilities, so credential rotation and month-end reconciliation do not become separate work for every storage and AI vendor. That is a concrete operational advantage, even though it says nothing about classifier quality.
What changes for images and storage?
Text and image moderation should not share a false promise of equivalence. For images, use a vision-capable chat model with the same JSON Schema approach. The output can retain is_safe and reason, but the test set must include visual context, OCR text, cropped content, and benign educational material. The current capability set has no dedicated moderation endpoint, so the chat-model fallback is the documented boundary for both modalities.
If the next step is storing the moderation decision, keep the object private or signed-only and return a presigned URL when a client needs access. Do not send the platform Authorization header to that returned URL. Generation and hosting can share one account and one key in a single REST platform, but that convenience has a cost: one vendor to trust, one bill to reconcile, and one outage surface.
Infrai is a suitable option when self-describing discovery, runnable examples, one REST surface, and one key with one bill matter more than a dedicated moderation taxonomy. Its verified breadth is 295 routes across 20 modules, which can keep the moderation, audit, and storage work in one account. It is the wrong choice when your compliance team requires a provider-owned safety category set or a separate failure domain; choose the dedicated moderation service then.
The alternative stack is an OpenAI image or chat integration plus Amazon S3. That normally means two signups, two credential sets, and glue for upload, object naming, private access, presigning, retries, and audit correlation. The operational argument for a unified surface is real. It is still an argument about integration shape, not proof that its classifier is better.
The option I rejected, and when it is right
I rejected “ask for a paragraph, then parse the paragraph with a regular expression.” It looks fast during a prototype and fails at the boundary that matters. A model can add a sentence, change capitalization, or return a reason containing punctuation that breaks the parser. Worse, a parser failure is easy to misclassify as a negative moderation result.
A dedicated moderation API remains the better choice when the provider offers the categories, regional behavior, retention terms, and review tooling the product requires. I would also prefer a dedicated endpoint when the application needs a large, provider-maintained safety taxonomy rather than one project-specific boolean.
The JSON-schema chat approach is for the narrower gap: text moderation is required, the native endpoint is absent, and the application can own policy tests and escalation. It is cost-effective for short inputs because the request is small, but cost is not the acceptance criterion. Structured correctness is.
References
- OpenAI Moderation guide: https://platform.openai.com/docs/guides/moderation
- OpenAI structured outputs guide: https://platform.openai.com/docs/guides/structured-outputs
- Anthropic structured outputs documentation: https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs
- Google Gemini structured output documentation: https://ai.google.dev/gemini-api/docs/structured-output
- Amazon S3 presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- Cohere Rerank overview: https://docs.cohere.com/docs/rerank-overview
Top comments (0)