Files
dify/docs/BACKEND_MODERATION.md

351 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Strict moderation for model invocations
`ModerationService.check` is a reusable use case for the existing `keywords`,
`openai_moderation`, and `api` capabilities. It receives an admitted
`AppRequestContext`, a `ModerationRule`, text, and an `input` or `output` phase.
It does not depend on Flask, persistence models, or app queue managers.
An enabled phase returns only after approval. Any flagged verdict raises
`ModerationRejectedError`, including API verdicts requesting replacement rather
than rejection. Backend failures propagate instead of silently approving text.
Disabled phases perform no moderation I/O. Strict policies are selected when
assembling models. App OpenAI rules consume typed platform failures to apply
configured replacement behavior; they never schedule a second OpenAI check.
## Automatic platform moderation
`HOSTED_MODERATION_ENABLED` controls automatic platform moderation in Cloud and
defaults to false. It takes effect only when `DEPLOYMENT_EDITION=CLOUD`, matching
the edition boundary used to assemble AI Credits quotas. Non-Cloud deployments
ignore the switch. When disabled or outside Cloud, no automatic policy is
constructed or injected. Explicit model policies and app-level keyword/API
moderation remain available in every edition. App-level Model Moderation is a
silent no-op when the model is not admitted for automatic AI-credit moderation;
enabling its app configuration does not select a tenant moderation provider.
When enabled,
Cloud LLMs using a system provider with a current `TRIAL` or `PAID`
credit-pool quota automatically receive input and output moderation.
This includes `CREDITS` and legacy `TIMES` quotas, which reserve one credit per call.
This applies to manager-created models, credential-cache hits, and direct `ModelInstance`
construction, including exhausted or unlimited credit quotas. Cloud custom-key,
non-LLM, and token-based quota modes do not receive this automatic policy.
Merely configuring a fallback identifier does not enable moderation.
For example, an operator can enable moderation and pin an optional fallback:
```dotenv
DEPLOYMENT_EDITION=CLOUD
HOSTED_MODERATION_ENABLED=true
HOSTED_OPENAI_API_KEY=<server-owned-api-key>
HOSTED_MODERATION_PLUGIN_UNIQUE_IDENTIFIER=langgenius/openai:<version>@<checksum>
```
The policy uses `langgenius/openai/openai` / `omni-moderation-latest` with the
server-owned `HOSTED_OPENAI_API_KEY` and optional OpenAI base/organization settings.
These credentials do not require enabling an OpenAI LLM trial/paid provider or
initializing Cloud hosting quotas. It first discovers the tenant's
`langgenius/openai/openai` provider and requires a Marketplace installation,
moderation capability, the official package identity, and a verified manifest.
If that route is absent, untrusted, or fails, it uses
`HOSTED_MODERATION_PLUGIN_UNIQUE_IDENTIFIER`, a pinned
`langgenius/openai:<version>@<checksum>` fallback supplied by SRE. A positive
violation verdict is final: it never triggers fallback to obtain another verdict.
The fallback identifier is validated only if needed; a working trusted tenant
plugin does not require it. Missing both routes, platform credentials, or a
working backend fails closed. A user's API key is never substituted.
The verified tenant route uses `/plugin/{tenant_id}/dispatch/moderation/invoke`,
which authorizes the plugin through the tenant's installation record. The SRE
fallback instead uses `/v2/invoke/dispatch/moderation/invoke` with a mandatory
`X-Plugin-Unique-Identifier` and the admitted `tenant_id` in JSON. Both routes
keep daemon server-key authentication, platform credentials, and pinned package
identity. An arbitrary client request cannot select direct dispatch or replace
the server-managed fallback identifier.
The v2 route is the daemon's existing direct-runtime invocation surface, originally
introduced for Slim remote CLI calls. It parses the supplied identifier, preserves
tenant execution context and cluster routing, and does not query tenant plugin
installations. It is not an installation endpoint or a public tenant API. The
fallback runtime must already be deployed and available to the daemon; direct
dispatch does not bootstrap a missing package. The fallback remains an SRE-owned
trust decision, while tenant-selected packages still require Marketplace and
verified-manifest checks. Moderation does not construct a
tenant model instance or enter `CreditPoolService`, so it neither reserves nor
deducts user credits. The target LLM retains its normal accounting, including
charging consumed usage when output is rejected.
The former `HostingModerationFeature` and `HOSTED_MODERATION_PROVIDERS` allowlist
have been removed. `HOSTED_MODERATION_ENABLED` controls the Cloud platform policy.
The old
random input-fragment check is replaced by complete segmented checks.
An explicitly supplied policy supplements enabled platform moderation rather
than disabling it. When the switch is off, only the explicit policy applies.
## App-level ownership and replacement
Each model guard exclusively owns its input
and output checks. App Model Moderation never proactively invokes OpenAI, selects
credentials, or caches approved content. Its configuration only selects a reply
for a real platform rejection. Disabling an app phase does not disable mandatory
platform checks. Different model invocations remain independent even when their
text is identical.
App checks and reply selection are stateless functions of the admitted app
configuration. There is no moderation scope, context variable, failure queue,
observer, or cross-thread model-to-response side channel. Parallel workflow model
calls do not serialize their moderation requests.
Startup injects one stateless policy port into the application container; response
pipelines retain that port, not an invocation-scoped session. The kernel does not
import service implementations or require a moderation wrapper around each entry.
Graphon node results retain exception type names, but terminal graph events only
carry display text. The Dify workflow runner propagates the failing node's type
through an internal field on its terminal queue event. Container failures inherit
their failed child's type; handled failures and retries do not classify a later
terminal failure. Platform input and output rejections have distinct exception
types so phase-specific presets survive this boundary without parsing messages.
Because Graphon has no terminal failure source ID, container error text correlates
its selected failure with typed child events. Text never supplies a type or phase;
conflicting child types retain the generic error instead of inventing a verdict.
The internal field is excluded from serialization and does not change public SSE.
The parameter-extraction node has a small compatibility adapter because Graphon
currently drops even the exception type when catching model invocation failures.
It retains only that type during one node run and clears it on every exit.
Keyword and API app rules still use `ModerationService.evaluate` and retain their
direct-reply and text-override semantics. They are independent policies, not
duplicate OpenAI checks, and cannot bypass platform moderation. Output checks run
sequentially on the response consumer without a worker or evaluation lock.
Streaming checks inspect only new segments; completion checks only an unchecked
tail, never the whole stream again. Direct replies replace the whole answer;
API overrides replace the flagged segment while retaining preceding output and
later deltas. Blocking output uses the non-stream segment size. Segments have no
overlap or cross-boundary context. The existing app-level display-then-replace
behavior remains distinct from the platform guard's check-before-delivery guarantee.
Application entry points call the same stateless app adapters; no request wrapper
is required to enable the correct behavior. Standalone kernel callers without an
application container retain the keyword/API factory fallback. Their App OpenAI
toggle is still a no-op and never selects an independent tenant provider.
Independent use of the OpenAI capability retains tenant credentials, accounting,
and failure behavior. It is an explicit independent
operation, not an execution path selected by the App Model Moderation toggle.
The plugin `/invoke/app` entry uses the same app generator behavior. Invocation
validates tenant ownership before model work. Each workflow runner owns its
event state, so nested or subsequent requests cannot inherit another run's failures.
Remote Agent LLM gateway calls do not transport local workflow node error types.
Local App Model Moderation never issues hosted or tenant
checks and cannot infer a typed remote verdict from text. Other configured app
rules remain independent. A client-provided approval flag must never skip platform checks.
An enabled app OpenAI rule uses its nonblank phase-specific preset when the
platform rejects content. Chatbot, Chatflow, and legacy Agent Chat use the
English default `This message can’t be processed.` when no matching
preset is configured, including when app moderation or its phase is disabled.
The default is resolved at runtime, not written into the app configuration.
Other app moderation backends retain their own verdict and replacement rules;
their presets are not reused for a separate platform rejection.
Chat replies use the existing `message` or `message_replace` event followed by
`message_end`, without exposing the rejected segment. A replacement covers the
entire answer, including any previously delivered prefix, and the stored answer
matches the replacement. Rendering it does not invoke a model or estimate token
usage again. A failed chatflow retains its failed workflow status while rendering
the failing node's platform rejection with its preset or default reply.
The reply and `message_end` precede the failed `workflow_finished` event because
the Redis/SSE subscriber stops reading at that terminal event; sending the reply
afterwards would persist it without ever delivering it to the WebApp client.
Unavailable checks are errors, not content verdicts, and never become replies.
Chatflow restores the typed `moderation_unavailable` error from the failing node's
error type. Error text alone cannot classify an unrelated failure, and no request
state is shared between invocations.
The error event precedes `workflow_finished` so public subscribers receive its
503 payload, while blocking requests raise the typed error for HTTP mapping.
No unavailable check becomes a preset or default chat reply.
Explicit non-platform model policy failures are also preserved. Completion,
Workflow, RAG, and other non-chat paths retain the normal moderation error when
there is no matching preset. Remote New Agent errors retain their existing
transport handling; arbitrary upstream errors are not inferred to be platform
rejections from their text.
Platform rejections emit warning logs containing tenant, app, and phase.
Tenant-route failures also log fallback warnings. Moderated content and API keys
are never included in these log messages.
## App errors
Model-bound guards adapt moderation failures to the existing `AppGenerateError`
contract, so task error handling preserves them and response converters emit
the usual SSE `error` event. Console clients use their existing error toast:
Application errors carry stable moderation identifiers and safe messages, not
HTTP status codes or response payloads. HTTP handlers and the SSE response
converter own status mapping. Keyword/API input and output backend failures are
adapted to the same unavailable error; private diagnostics remain in the cause
and server logs, never in the client-facing response.
| Condition | Code | Event status | Message |
| --- | --- | --- | --- |
| Content rejected | `moderation_rejected` | 400 | The configured rejection message, or the phase-specific fallback |
| Check unavailable or failed | `moderation_unavailable` | 503 | This message can’t be processed right now. Please try again later. |
These statuses describe the SSE error payload, not the HTTP status of an already
opened stream. Non-streaming requests on Console, Service API, and Web API return
the same codes and messages with the corresponding HTTP status. Check failures
retain their original cause in backend logs but
never expose platform credentials or plugin diagnostics to clients. Provider
generation failures retain their existing classification. When an error is
surfaced, approved segments remain visible; neither error delivers the rejected
or unchecked segment.
Chat apps may render a platform rejection using a matching preset or the default
reply described above instead of returning this error payload.
App output evaluation failures use the same `moderation_unavailable` contract.
The response consumer immediately stops consuming queued output, persists the
message's error status, and emits the existing error event. The handler retains
the first failure without locks; completion cannot retry it into approval.
Already delivered app-level output is not retracted by this error; platform
model guards continue to check segments before delivery.
## Model invocation
Obtain the service from the composition root and bind the policy during model
assembly. Individual model invocations never accept a moderation override:
```python
from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly
from extensions.application_services.moderation import build_llm_moderation_guard
from extensions.ext_application_services import application_services
from graphon.model_runtime.entities.model_entities import ModelType
from machinery.context import AppRequestContext
from services.moderation_service import ModerationRule
guard = build_llm_moderation_guard(
AppRequestContext(tenant_id=tenant_id, app_id=app_id),
service=application_services().moderation,
rule=ModerationRule(
type="keywords",
config={
"keywords": "blocked phrase\nanother phrase",
"inputs_config": {"enabled": True, "preset_response": "Input rejected"},
"outputs_config": {"enabled": True, "preset_response": "Output rejected"},
},
),
)
assembly = create_plugin_model_assembly(tenant_id=tenant_id, moderation=guard)
model_instance = assembly.model_manager.get_model_instance(
tenant_id=tenant_id,
provider=provider,
model_type=ModelType.LLM,
model=model_name,
)
result = model_instance.invoke_llm(prompt_messages=prompt_messages, stream=False)
```
Use the same configuration shape as app moderation. OpenAI uses
`type="openai_moderation"`; API extensions use `type="api"` with
`api_based_extension_id` scoped to the admitted tenant. An empty `app_id` is
allowed for non-app invocations; API extensions receive it unchanged. The guard
snapshots the configuration so later caller mutations cannot disable its checks.
`ModelManager.for_tenant(..., moderation=guard)`,
`create_plugin_model_manager(..., moderation=guard)`, and direct
`ModelInstance(..., moderation=guard)` construction support the same binding.
The policy follows every manager-created LLM, including cached credentials and
default-model resolution. It is exposed read-only on the instance, and mismatched
tenant bindings fail before credential resolution. Non-LLM capabilities do not
inherit LLM policies, so moderation-model calls do not recursively moderate themselves.
The assembly owner supplies a trusted scope for explicit policies. Automatic
selection uses trusted deployment and provider quota configuration, never
caller-provided request metadata. Raw plugin runtime/client interfaces remain
infrastructure, not moderated business entry points.
Both segment limits are required when constructing a guard. Production assembly
always provides the configured positive limits; there is no alternate unbounded
whole-stream buffering mode selected by an omitted limit.
## Guarantees and trade-offs
- Input checks precede model invocation and quota reservation. All prompt roles,
text parts of multimodal messages, and assistant tool arguments are inspected
as independent non-stream segments, so long prompts are fully covered without
sending one oversized moderation request. Each platform-checked prompt message
has its own segment sequence. Each model invocation performs its own checks;
app response handling does not schedule another moderation request.
- Opaque Agent gateway dictionaries are inspected through validated copies. The
original payload and provider-specific extension fields are forwarded unchanged.
- Output checks inspect completion text, tool arguments, and native structured
payloads even when their message text is empty. Binary image, audio,
video, and document content is not inspected by these text moderation backends.
- Non-stream output is checked in independent segments of
`MODERATION_NON_STREAM_SEGMENT_SIZE` characters. Every segment must pass before
the original result is returned.
- Streaming output is buffered until it reaches
`MODERATION_BUFFER_SIZE` characters, then checked before its original
chunks are released. The final partial segment is also checked before release.
Provider chunks are kept intact, so a segment can exceed the configured threshold.
A later rejection stops the stream and preserves earlier delivered segments;
current model-level consumers cannot retract those bytes.
- Segmentation is the moderation boundary: each segment is checked independently,
without carrying text across segment boundaries or rechecking the full document
at completion. Content requiring cross-segment context may not be detected.
Defaults are 2000 characters for input and non-stream output, and a 300-character
buffering threshold for streaming output in both app-level and model-level moderation.
- Consumed model usage is settled before output checking, including rejected
completions. A provider or moderation error is not an approval or a model retry.
- Structured output automatically uses the policy of its model instance. Workflow
polling applies that same policy before task submission and to successful terminal
results, after settling usage. Rejected inputs neither submit a task nor reserve
model quota; rejected successful outputs do not refund consumed quota.
- The guard protects returned results, not provider-level observer callbacks.
Callbacks must not independently publish unmoderated model content.
- Models outside enabled automatic platform moderation retain their existing invocation and
streaming behavior unless an explicit policy is supplied.
The API adapter loads tenant-scoped extension records in a short repository
session, then decrypts credentials and performs HTTP outside that session. It
requires an explicit boolean `flagged` field in every API verdict.
## RAG failure handling
`ModerationInvocationError` identifies model moderation failures without exposing
application-service dependencies to RAG consumers. Both rejection and service
unavailability remain terminal for the affected operation, not best-effort empty
results. Input failures precede model usage; output failures retain consumed
model charges.
QA formatting propagates moderation failures in preview and indexing mode.
Bounded worker futures return failures to the indexing coordinator, which marks
the document as errored instead of completing an empty or partial QA index.
Workers retain ten-chunk batches, so a failed batch does not submit subsequent
batches or incur further model calls for them.
Ordinary QA generation failures retain their existing best-effort handling.
Function-call dataset routing also propagates moderation failures. Ordinary
routing failures may still yield no selected dataset, but moderation failure
must never be interpreted as a successful empty retrieval.
Automatic metadata filtering in both legacy RAG and KnowledgeFS stops on
moderation failures instead of continuing with no automatic conditions.
KnowledgeFS emits a failed node event with the moderation exception type and
does not issue a retrieval request. Ordinary extraction and parsing failures
keep their existing fallback behavior.
Paragraph and parent-child summary previews preserve moderation failures from
their worker futures. Indexing-estimate adapters and application services pass
the shared moderation error contract through unchanged, so existing HTTP
handlers return `moderation_rejected` (400) or `moderation_unavailable` (503)
instead of a generic indexing-estimate error. Ordinary preview failures keep
their existing aggregated error messages.
Background summary generation records `ERROR` on the affected summary and does
not save or vectorize the failed LLM output. Other segments can still finish;
the original document's indexing status is independent of its summary status.