Skip to main content

Transparency

AI.Sentinel can disclose that a reply was generated by AI. Turned on, it adds a machine-readable mark to every reply that carries assistant text, and optionally a human-readable notice at the start of the reply. It is off by default, so existing behaviour does not change.

This helps with the transparency duties of EU AI Act Art. 50, such as telling people they are dealing with an AI system and marking AI-generated output in a machine-readable way. It is a building block, not a compliance verdict: whether and how Art. 50 applies to your system, and whether this is enough, stays with you as provider or deployer.

services.AddAISentinel(opts =>
{
opts.Transparency.Enabled = true;
});

Options​

OptionDefaultMeaning
Transparency.EnabledfalseTurns the mark and the notice on.
Transparency.Notice"This answer was generated by AI."The text of the notice.
Transparency.NoticeModeOncePerConversationWhen the notice is added: OncePerConversation, EveryReply or Never.

Never leaves the notice out and keeps the mark, for an application whose own UI already tells people they are talking to an AI. With transparency enabled, an empty or whitespace Notice is rejected when you call AddAISentinel, unless NoticeMode is Never. So is a NoticeMode value that is not one of the three modes.

services.AddAISentinel(opts =>
{
opts.Transparency.Enabled = true;
opts.Transparency.Notice = "Dit antwoord is gegenereerd door AI.";
opts.Transparency.NoticeMode = TransparencyNoticeMode.EveryReply;
});

The machine-readable mark​

The mark is a set of entries in ChatResponse.AdditionalProperties. The key names are constants on TransparencyKeys.

KeyValue
ai.generatedtrue
ai.model_idThe model that answered: ChatResponse.ModelId, else ChatOptions.ModelId. Absent when neither is known.
ai.generated_atWhen the reply was marked, as an ISO 8601 UTC timestamp string.
ai.pipelineThe name of the named pipeline that marked the reply. Absent for the default pipeline.

If the inner client already set a value for a key, it is kept. The exception is ai.generated, which is always true.

var response = await client.GetResponseAsync(messages, options);
if (response.AdditionalProperties?.ContainsKey(TransparencyKeys.Generated) == true)
{
// AI-generated
}

Streaming​

A streamed reply is marked too. The keys go on the first update that has no MessageId, and ToChatResponse() carries them onto the ChatResponse it builds. When every update has a MessageId, the keys go on the first update and land on the first message. That can be a tool-call message rather than the message with the text. So look for the keys on the response first, then on its messages.

The notice​

The notice is added as leading text of the last assistant message that has visible text, after the scans and after redacted values are restored. The layer's own detectors never judge it, and it is never taken for model output. An outer Sentinel layer is the exception, see Stacked layers.

  • A reply that only calls tools has no visible text and is not disclosed. The reply that follows the tool results is.
  • A quarantined reply, a reply for which the inner client failed, and a reply whose redacted values could not be restored are not disclosed. The caller gets an error, not an answer.
  • With streaming, the notice is a new leading piece of text in the first update with text of the last assistant message with text.

Modes​

ModeNotice added to
OncePerConversationThe first disclosed reply of each conversation.
EveryReplyEvery disclosed reply.
NeverNo reply. The mark is still set.

Giving Sentinel a conversation​

OncePerConversation needs to know which calls belong together. Sentinel takes the session key from sentinel.session_id in ChatOptions.AdditionalProperties, or else from ChatOptions.ConversationId:

var options = new ChatOptions
{
AdditionalProperties = new() { ["sentinel.session_id"] = conversationId },
};

Without a session key, every reply gets the notice, because Sentinel cannot tell that two calls are one conversation.

Sentinel remembers which sessions it has disclosed in the pipeline itself:

  • The memory belongs to the built chat client and to the pipeline. An application that builds a new chat client for each request shows the notice every time.
  • A multi-instance deployment tracks per instance, so a conversation that moves between instances can see the notice once on each.
  • A session is forgotten after SessionIdleTimeout (one hour by default), and the next reply starts the conversation again.

Function invocation​

With Sentinel outside UseFunctionInvocation, the whole tool loop is one Sentinel call, and the answer gets one notice. With Sentinel inside it, as in builder.UseFunctionInvocation().UseAISentinel(), each round trip to the model is its own Sentinel call, and each round trip that returns assistant text is a disclosed reply. A model that says "Let me check." before calling a tool and then gives the answer has two replies. Pass sentinel.session_id or ConversationId to get one notice and one DISCLOSED entry per conversation; without a session key each of those round trips gets the notice and writes an entry.

History​

When the application keeps the conversation and sends it back, the notice Sentinel added is part of the earlier assistant messages. Sentinel removes it from the start of assistant messages before scanning and sending, so the model never sees it and does not learn to copy it. User messages are never changed. The history you pass in is not modified; Sentinel works on copies.

Only the notice at the start of an assistant message is removed. The same words anywhere else in a message stay.

Audit​

The first disclosure of a conversation is written to the audit store as one informational entry:

FieldValue
DetectorIdTRANSPARENCY
PolicyCodeDISCLOSED
SeverityNone
SessionIdThe conversation's key
SummaryDisclosed: machine-readable mark and notice, or Disclosed: machine-readable mark when no notice was added

A call without a session key has no conversation, so it writes an entry for every reply. Like redactions and approval decisions, the entry is evidence that something happened, not a detection: it is never counted as a threat. The dashboard shows it with its own Disclosed badge, and the Disclosed chip lists only these entries. If the entry cannot be written, the call fails with the exception and the reply is not delivered.

Named pipelines​

Each named pipeline has its own options, so enable transparency on the pipelines that need it. A reply from a named pipeline gets the ai.pipeline key with the pipeline's name.

Stacked layers​

If you stack two Sentinel layers, enabling transparency on both gives the reply the notice twice and writes two DISCLOSED entries; the mark keeps the inner layer's values. The outer layer's detectors scan the reply as the inner layer returns it, so they see the inner layer's notice. Enable transparency in one layer, preferably the outer one, so that every layer's detectors judge the reply without the notice.

Scope​

Transparency covers the in-process IChatClient pipeline. The hook CLIs (sentinel-hook, sentinel-copilot-hook) and the MCP proxy (sentinel-mcp) are out of scope. They do not mark replies or add a notice.