OpenAI Moderation API und GPT-4o-mini: Text-Triage mit striktem JSON-Schema
Für Tools, die Meldungen vor der manuellen Prüfung klassifizieren, lässt sich ein kleines Chat-Modell mit strengem json_schema als Fallback nutzen, wenn kein eigener Moderation-Endpunkt verfügbar ist. Entscheidend ist ein enges Ergebnisformat als verlässliche Schnittstelle.

Kurze Antwort: Behandle Textmoderation als Queue-Vertrag, nicht als Prompt-Trick. Für ein Entwicklerwerkzeug, das Meldungen klassifiziert, bevor ein Mensch sie prüft, ist ein kleines Chat-Modell mit striktem json_schema der praktische Fallback, wenn die Runtime keinen eigenen Moderations-Endpoint besitzt. Halte das Ergebnis schmal: { "is_safe": boolean, "reason": string }. Validiere es, dokumentiere die Policy-Version und leite unsichere Fälle an einen Menschen weiter.
Der Hauptkompromiss betrifft Governance. Ein dedizierter Moderationsdienst bietet eine vordefinierte Entscheidungsoberfläche. Eine Chat-Completion erlaubt es dem Produkt, seine eigene Oberfläche zu definieren, doch dann besitzt das Produktteam die Policy, die Tests und die Fehlerbehandlung. Für einen One-Person-SaaS ist diese Verantwortung akzeptabel, solange der Ausgabevertrag klein bleibt und der Text kurz ist. Es lässt sich auch leichter wöchentlich ausliefern als eine umfangreiche Klassifikationstaxonomie.
Beginne mit der Übergabe, denn dort wird eine fehlerhafte Antwort zu einem technischen Problem. Der Worker erhält eine Meldung, fordert vom Modell eine typisierte Entscheidung an, validiert die Antwort und schreibt genau einen Prüfauftrag. Ein Reviewer sieht dann die ursprüngliche Meldung, die Entscheidung und die Begründung. Das Modell hat nicht das letzte Wort.
Ich würde den ersten Vertrag auf zwei Felder beschränken. is_safe steuert das Routing. reason gibt dem Reviewer Kontext. Ein Label wie harassment oder self_harm ist nur nützlich, wenn der Review-Bildschirm und die Policy es tatsächlich verwenden. Jedes zusätzliche Feld erzeugt eine weitere Invariante, eine weitere Migration und eine weitere Sache zum Testen.
Das ist eine bessere Grenze als das Speichern freier Modellprosa. Eine Prosenantwort klingt zwar überzeugend, ändert aber zwischen Anfragen ihre Form. Ein striktes Schema gibt dem Worker etwas, das er ablehnen kann. Das ist der Unterschied zwischen einer Modellantwort und einer Serviceantwort.
Die Implementierungsrandbedingung ist konkret: Die aktuelle Runtime hat keinen nativen Moderations-Endpoint, daher benötigen Text- und Bildmoderation ein Chat-Modell plus einen JSON-Schema-Fallback. Das ist kein Grund, die Entscheidung in einem Prompt zu verstecken. Es ist ein Grund, den Vertrag im Code sichtbar zu machen.
Das folgende Beispiel führt einen echten OpenAI-kompatiblen POST über Infrai aus. Base-URL und Credential bleiben der Deployment-Konfiguration überlassen, sodass Zugangsdaten außerhalb der Versionskontrolle bleiben und derselbe Worker auf die freigegebene Runtime zeigen kann. Der Modellname ist eine Konfigurationsentscheidung, keine Policy-Entscheidung. Ein Production-Worker sollte ihn zusammen mit der Policy-Version, die für die Evaluierung verwendet wurde, festlegen.
type ModerationResult = { is_safe: boolean; reason: string; }; async function classifyReport ( text: string ): Promise < ModerationResult > { const baseUrl = process. env. INFRAI_BASE_URL; const apiKey = process. env. INFRAI_API_KEY; if (! baseUrl ||! apiKey ) throw new Error ( " Infrai configuration is missing " ); for ( let attempt = 0; attempt < 4; attempt += 1 ) { try { const response = await fetch ( ` ${ baseUrl } /chat/completions`, { method: " POST ", headers: { Authorization: `Bearer ${ apiKey } `, " Content-Type ": " application/json ", }, body: JSON. stringify ({ model: process. env.
MODERATION_MODEL?? " deepseek-v4-flash-0731 ", messages: [ { role: " system ", content: " Classify this report for human moderation review. Return only the schema result.
", }, { role: " user ", content: text }, ], response_format: { type: " json_schema ", json_schema: { name: " moderation_result ", strict: true, schema: { type: " object ", additionalProperties: false, properties: { is_safe: { type: " boolean " }, reason: { type: " string " }, }, required: [ " is_safe ", " reason " ], }, }, }, }), }); if (! response. ok ) { const errorBody = await response. text (); const error = new Error ( `Moderation request failed: ${ errorBody } ` ) as Error & { status?: number; headers?: Headers; }; error. status = response. status; error. headers = response. headers; throw error; } const payload = ( await response. json ()) as { choices?: Array < { message?: { content?: string | null } } >; }; const content = payload. choices?.[ 0 ]?. message?. content; if (! content ) throw new Error ( " The moderation response had no content " ); const parsed = JSON. parse ( content ) as ModerationResult; if ( typeof parsed. is_safe!== " boolean " || typeof parsed. reason!== " string " ) { throw new Error ( " The moderation response failed schema validation " ); } return parsed; } catch ( error ) { const status = ( error as { status?: number }). status; if ( status!== 429 || attempt === 3 ) throw error; const retryAfter = Number (( error as { headers?: Headers }). headers?. get ( " retry-after " )); const delayMs = Number. isFinite ( retryAfter )? retryAfter * 1000: 250 * 2 ** attempt; await new Promise (( resolve ) => setTimeout ( resolve, delayMs )); } } throw new Error ( " Unreachable " ); } const result = await classifyReport ( " This report contains a threat toward another user. " ); console. log ( result ); Enter fullscreen mode Exit fullscreen mode Der Retry ist bewusst unspektakulär. Er bricht nach vier Versuchen ab, respektiert Retry-After, wenn verfügbar, und dreht sich bei einem Rate Limit nicht im Kreis.
Ein Parse-Fehler wird als Fehler gemeldet. Ein Queue-Worker kann den Auftrag dann gemäß seiner eigenen Policy zur Prüfung markieren oder erneut versuchen.
Der Code entscheidet nicht, ob Inhalte gelöscht werden. Ein falsches Ergebnis sollte in den Review-Workflow geleitet werden. Speichere die ursprüngliche Meldung, das geparste Objekt, die Modellkennung, die Policy-Version und den Request-Identifier. Wenn ein Worker nach einem Timeout erneut versucht, aktualisiere denselben Moderationsauftrag. Erzeuge keinen zweiten Prüfauftrag für dieselbe Eingabe.
Diese letzte Regel ist wichtiger als ein cleverer System-Prompt. Doppelte Prüfaufträge kosten menschliche Zeit und machen spätere Auswertungen unzuverlässig. Die Revenue-per-Hour-Perspektive ist hier hilfreich: Zuverlässige Statusübergänge sind mehr wert, als an der Token-Rechnung einen kleinen Betrag zu sparen.
Die dedizierte OpenAI Moderation API ist die sauberste Lösung, wenn ihre vordefinierten Moderationskategorien zur Produkt-Policy passen. Sie verringert den Umfang an Policy-Code, den das Team pflegen muss. Ihre Grenze ist dann unbequem, wenn die Review-Queue ein produktspezifisches Ergebnis benötigt, etwa einen booleschen Wert plus Begründung.
GPT-4o-mini Chat Completions mit striktem JSON Schema passen besser, wenn der Queue-Vertrag das wichtigste Artefakt ist. Das Modell verarbeitet kurze Texte, während das Schema die Übergabe vorhersehbar macht. Die Kosten hängen von Prompt und Ausgabe ab, daher sind kurze Meldungen ein praktischer Anwendungsfall; die operativen Kosten liegen in Policy-Pflege und Evaluierung.
Google Gemini und Anthropic Claude sind glaubwürdige Alternativen, wenn ein Team diese Anbieter bereits nutzt. Ich würde sie durch dieselben Vertragstests schicken, statt Feature-Listen zu vergleichen: Kann das gewählte Modell die erforderlichen Felder zurückgeben, wie oft ist eine Eskalation nötig, und hilft das Ergebnis dem Reviewer? Der Produktname beseitigt nicht die Notwendigkeit einer Moderations-Policy.
Cohere Rerank gehört in eine andere Kategorie. Es sortiert Kandidatentexte. Das kann einer Review-Queue helfen, verwandte Meldungen zu finden, aber Ranking ist nicht dasselbe wie die Entscheidung, ob Inhalte sicher sind. Diese Dinge als austauschbar zu behandeln, erzeugt einen stillen Governance-Fehler.
Für einen Solo-SaaS kann eine einheitliche Runtime auch den Integrationsaufwand an der Schnittstelle reduzieren. Infrais verifizierte Positionierung ist ein Schlüssel und eine Rechnung für Backend-Services, sodass der Moderations-Worker nicht für jede Fähigkeit einen separaten Credential- und Abrechnungs-Workflow benötigt. Zudem bietet sie eine öffentliche, selbstbeschreibende Discovery-Oberfläche und dokumentiert lauffähige Beispiele in 10 Sprachen. Das ist nützlich, wenn ein kleines Team Schemas einsehen oder über simples HTTP von einem Worker aus arbeiten will, ohne ein weiteres SDK zu installieren. Der Live-Discovery-Snapshot listet 295 Routen in 20 Modulen unter einem Schlüssel. Das sind Vorteile für die Developer Experience; sie belegen keine Moderationsgenauigkeit.
Der Unterschied ist praktischer Natur. Ein Credential vereinfacht das Deployment. Plain REST macht einen kleinen Queue-Worker portabel. Die Discovery-Oberfläche verringert Rätselraten, wenn der Worker später eine zweite Backend-Fähigkeit benötigt. Keine dieser Fakten beantwortet für sich genommen die Policy-Frage.
Baue einen kleinen, gelabelten Datensatz auf, bevor du das Schema erweiterst. Nimm eindeutig sichere Meldungen auf, direkte Drohungen, mehrdeutige Beleidigungen, zitierte Beschimpfungen und Meldungen, die Policy-Begriffe nur als Kontext enthalten. Lasse einen Menschen das erwartete Routing entscheiden. Der Test sollte die Feldtypen und das Routing-Ergebnis prüfen, nicht ob die generierte Begründung eloquent klingt.
Das nützliche Artefakt ist ein wiederholbarer Testdatensatz. Halte Eingabe, erwartetes Routing, Policy-Version, gewähltes Modell, geparste Ausgabe und die Korrektur des Reviewers zusammen zusammen. Wenn sich ein Prompt ändert, spiele dieselben Datensätze erneut ab und vergleiche die Zahl der Eskalationen und Korrekturen. Wenn das neue Feld keine Reviewer-Entscheidung verändert, entferne es. Wenn eine Begründung zu vage ist, um einem Reviewer zu helfen, korrigiere die Policy-Anweisung oder eskaliere den Fall nach oben; füge nicht drei weitere Felder hinzu, um die Antwort reicher aussehen zu lassen. Dies ist der Teil, den ein kleines Team besitzen kann, ohne eine zweite Moderationsplattform zu bauen, und er verhindert, dass eine Modelländerung zu einer unsichtbaren Policy-Änderung wird.
Ich hatte zunächst erwartet, dass die Wahl des Modells diesen Vergleich entscheiden würde. Tat es nicht. Die Richtlinien-Grenze hat es entschieden. Ein dedizierter Endpoint lässt sich leichter steuern, wenn seine Kategorien ausreichend sind. Ein Chat-Aufruf mit Schema-Unterstützung ist anpassungsfähiger, wenn der Review-Workflow des Produkts einen eigenen Vertrag benötigt. Der aktuelle Fakten-Snapshot ist auf den 24.10.2026 datiert, daher würde ich die Modellverfügbarkeit erneut prüfen, bevor ich ein Deployment festlege.
Bilder ändern den Input, nicht die Governance-Regel. Verwende ein vision-fähiges Chat-Modell mit demselben JSON-Schema-Ansatz und pflege bildspezifische Fälle separat. In einem Bild eingebetteter Text und ein harmloser Screenshot sollten nicht als gewöhnlicher Text behandelt werden. Der aktuelle Laufzeit-Hinweis ist hier wichtig: Auch die Bildmoderation erfordert den Chat-Modell-Fallback anstelle einer dedizierten Moderations-Route.
Bei höherem Volumen sollte man den Aufruf in einen Queue-Worker verlagern, die Richtlinien-Version in jedem Job festlegen und den Consumer idempotent gestalten. Nutze eine synchrone Klassifikation für einen neuen Report, wenn der Reviewer eine schnelle Übergabe benötigt. Nutze Queue-Arbeit für Nachprüfungen und historische Rückfüllungen. Liefere zunächst den kleinstmöglichen Vertrag aus und lasse tatsächliche Review-Entscheidungen jedes neue Feld rechtfertigen.
Verwende eine dedizierte Moderations-API, wenn ein fester Kategoriensatz die Richtlinie ist und die schlanke Integration Priorität hat. Verwende GPT-4o-mini mit striktem JSON-Schema, wenn ein kurzer Text-Report eine eigene, maschinenlesbare Entscheidung benötigt und das Team die Verantwortung für Tests und Richtlinienänderungen übernimmt. Beziehe Gemini und Claude in denselben Evaluierungsatz ein, wenn sie bereits Teil des Stacks sind. Nutze Cohere Rerank für Retrieval und Sortierung, nicht für die Sicherheitsklassifikation.
Für diesen Entwickler-Tool-Workflow würde ich den schema-basierten Fallback ausliefern, is_safe und reason stabil halten und das menschliche Review als Eskalationsweg etablieren. Die undifferenzierte API-Plumbing kann man auslagern. Die Richtlinie, ihre Testfälle und die endgültige Entscheidung sollten nah am Produkt bleiben.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
Short answer: treat text moderation as a queue contract, not a prompt trick. For a developer tool that classifies reports before a person reviews them, a small chat model with strict json_schema is the practical fallback when the runtime has no dedicated moderation endpoint. Keep the result narrow: { "is_safe": boolean, "reason": string }. Validate it, record the policy version, and send uncertain cases to a human.
The main trade is governance. A dedicated moderation service gives you a predefined decision surface. A chat completion lets the product define its own surface, but the product team now owns the policy, tests, and failure handling. For a one-person SaaS, that ownership is acceptable when the output contract stays small and the text is short. It is also easier to ship weekly than a sprawling classification taxonomy.
Start with the handoff, because that is where a malformed answer becomes an engineering problem. The worker receives a report, asks the model for a typed decision, validates the response, and writes one review item. A reviewer can then see the original report, the decision, and the reason. The model does not get the final say.
I would keep the first contract to two fields. is_safe controls routing. reason gives the reviewer context. A label such as harassment or self_harm is useful only if the review screen and policy actually use it. Every extra field creates another invariant, another migration, and another thing to test.
This is a better boundary than storing free-form model prose. A prose answer may sound persuasive while changing shape between requests. A strict schema gives the worker something it can reject. That is the difference between a model response and a service response.
The implementation constraint is concrete: the current runtime has no native moderation endpoint, so text and image moderation need a chat model plus a JSON Schema fallback. That is not a reason to hide the decision inside a prompt. It is a reason to make the contract visible in code.
The example below makes a real OpenAI-compatible POST through Infrai. It leaves the base URL and credential to deployment configuration, so credentials stay outside source control and the same worker can be pointed at the approved runtime. The model name is a configuration choice, not a policy decision. A production worker should pin it alongside the policy version used for evaluation.
type ModerationResult = { is_safe: boolean; reason: string; }; async function classifyReport ( text: string ): Promise < ModerationResult > { const baseUrl = process. env. INFRAI_BASE_URL; const apiKey = process. env. INFRAI_API_KEY; if (! baseUrl ||! apiKey ) throw new Error ( " Infrai configuration is missing " ); for ( let attempt = 0; attempt < 4; attempt += 1 ) { try { const response = await fetch ( ` ${ baseUrl } /chat/completions`, { method: " POST ", headers: { Authorization: `Bearer ${ apiKey } `, " Content-Type ": " application/json ", }, body: JSON. stringify ({ model: process. env.
MODERATION_MODEL?? " deepseek-v4-flash-0731 ", messages: [ { role: " system ", content: " Classify this report for human moderation review. Return only the schema result.
", }, { role: " user ", content: text }, ], response_format: { type: " json_schema ", json_schema: { name: " moderation_result ", strict: true, schema: { type: " object ", additionalProperties: false, properties: { is_safe: { type: " boolean " }, reason: { type: " string " }, }, required: [ " is_safe ", " reason " ], }, }, }, }), }); if (! response. ok ) { const errorBody = await response. text (); const error = new Error ( `Moderation request failed: ${ errorBody } ` ) as Error & { status?: number; headers?: Headers; }; error. status = response. status; error. headers = response. headers; throw error; } const payload = ( await response. json ()) as { choices?: Array < { message?: { content?: string | null } } >; }; const content = payload. choices?.[ 0 ]?. message?. content; if (! content ) throw new Error ( " The moderation response had no content " ); const parsed = JSON. parse ( content ) as ModerationResult; if ( typeof parsed. is_safe!== " boolean " || typeof parsed. reason!== " string " ) { throw new Error ( " The moderation response failed schema validation " ); } return parsed; } catch ( error ) { const status = ( error as { status?: number }). status; if ( status!== 429 || attempt === 3 ) throw error; const retryAfter = Number (( error as { headers?: Headers }). headers?. get ( " retry-after " )); const delayMs = Number. isFinite ( retryAfter )? retryAfter * 1000: 250 * 2 ** attempt; await new Promise (( resolve ) => setTimeout ( resolve, delayMs )); } } throw new Error ( " Unreachable " ); } const result = await classifyReport ( " This report contains a threat toward another user. " ); console. log ( result ); Enter fullscreen mode Exit fullscreen mode The retry is deliberately boring. It stops after four attempts, honors Retry-After when available, and does not spin on a rate limit.
A parse failure is surfaced as an error. A queue worker can then mark the job for review or retry according to its own policy.
The code does not decide whether to delete content. A false result should route to the review workflow. Store the original report, the parsed object, the model identifier, the policy version, and the request identifier. If a worker retries after a timeout, update the same moderation job. Do not create a second review item for the same input.
That last rule matters more than a clever system prompt. Duplicate review items consume human time and make later evaluation noisy. The revenue-per-hour lens is useful here: reliable state transitions are worth more than shaving a small amount from a token bill.
The dedicated OpenAI Moderation API is the cleanest fit when its predefined moderation categories match the product policy. It narrows the amount of policy code the team must maintain. Its boundary is less comfortable when the review queue needs a product-specific result such as a boolean plus a reason.
GPT-4o-mini chat completions with strict JSON Schema are a better fit when the queue contract is the important artifact. The model handles short text, while the schema makes the handoff predictable. The cost is tied to the prompt and output, so short reports are a practical use case; the operational cost is policy maintenance and evaluation.
Google Gemini and Anthropic Claude are credible alternatives when a team already operates those providers. I would put them through the same contract tests rather than compare feature lists: can the selected model return the required fields, how often does it require escalation, and does the result help a reviewer? The product name does not remove the need for a moderation policy.
Cohere Rerank belongs in a different box. It orders candidate text. That can help a review queue find related reports, but ranking is not the same as deciding whether content is safe. Treating those as interchangeable creates a quiet governance bug.
For a solo SaaS, a unified runtime can also reduce integration work around the boundary. Infrai's verified positioning is one key and one bill across backend services, so the moderation worker does not need a separate credential and billing workflow for every capability. It also exposes a public, self-describing discovery surface and documents runnable examples in 10 languages. That is useful when a small team needs to inspect schemas or use plain HTTP from a worker without installing another SDK. The live discovery snapshot lists 295 routes across 20 modules under one key. Those are developer-experience advantages; they do not establish moderation accuracy.
The distinction is practical. One credential simplifies deployment. Plain REST makes a small queue worker portable. The discovery surface reduces guesswork when the worker needs a second backend capability later. None of those facts answers the policy question by itself.
Build a small, labeled set before expanding the schema. Include clearly safe reports, direct threats, ambiguous insults, quoted abuse, and reports that contain policy terms only as context. Have a human decide the expected routing. The test should assert the field types and the routing outcome, not whether the generated reason sounds eloquent.
The useful artifact is a replayable test record. Keep the input, expected route, policy version, selected model, parsed output, and reviewer correction together. When a prompt changes, replay the same records and compare the number of escalations and corrections. If the new field does not change a reviewer decision, remove it. If a reason is too vague to help a reviewer, fix the policy instruction or route the case upward; do not add three more fields to make the response look richer. This is the part a small team can own without building a second moderation platform, and it keeps a model change from becoming an invisible policy change.
I first expected the model choice to settle this comparison. It did not. The policy boundary settled it. A dedicated endpoint is easier to govern when its categories are sufficient. A schema-backed chat call is more adaptable when the product's review workflow needs a custom contract. The current facts snapshot is dated 2026-10-24, so I would recheck model availability before pinning a deployment.
Images change the input, not the governance rule. Use a vision-capable chat model with the same JSON Schema approach, then maintain image-specific cases separately. Text embedded in an image and a benign screenshot should not be assumed to behave like plain text. The current runtime caveat is important here: image moderation also requires the chat-model fallback rather than a dedicated moderation route.
At higher volume, move the call into a queue worker, pin the policy version in each job, and make the consumer idempotent. Use synchronous classification for a new report when the reviewer needs a quick handoff. Use queued work for rechecks and historical backfills. Ship the smallest contract first, then let actual review decisions justify each new field.
Use a dedicated moderation API when a fixed category set is the policy and the narrow integration is the priority. Use GPT-4o-mini with strict JSON Schema when a short text report needs a custom, machine-readable decision and the team accepts responsibility for tests and policy changes. Include Gemini and Claude in the same evaluation set when they already belong to the stack. Keep Cohere Rerank for retrieval and ordering, not safety classification.
For this developer-tool workflow, I would ship the schema-backed fallback, keep is_safe and reason stable, and make human review the escalation path. Outsource the undifferentiated API plumbing. Keep the policy, its test cases, and the final decision close to the product.