Table of Contents

AI configuration

Every key deciding what leaves this process for a model provider and what it may cost: what is scanned before it goes, which endpoint and model each capability reaches, what one question and one period may spend, what classification does with what it concludes, and what the two backfills work through. The tables read as the configuration reference says they do, and that page is the map to the rest of the sections.

SensitiveContent

What this deployment scans mail for before that mail is copied into a derived store or handed out. A configuration root of its own, because it is a property of the deployment rather than of its database, its accounts, or its providers, and because the switches it holds reach several of those at once. Sensitive-content scanning records what a finding is, what replaces it, and why a scanner that cannot answer refuses the operation it guards.

Both scanners are off by default, and an absent section is that default rather than a startup failure. Secrets runs in this process. Pii reaches an analyzer deployed beside it, configured in the block below, and switching it on with nowhere to ask fails startup rather than running unprotected.

Key Type Default Constraint Change
SensitiveContent:Secrets:Enabled bool false A scanner switched on with no detector registered fails startup restart
SensitiveContent:Secrets:Categories:<n> string unset Must name a category the scanner detects; the list replaces the scanner's defaults, and an absent list yields them restart
SensitiveContent:Secrets:Suppressions:<n>:Category string Must name a category the scanner detects; naming one never switches it on restart
SensitiveContent:Secrets:Suppressions:<n>:Rule string Must name a rule that category holds restart
SensitiveContent:Pii:Enabled bool false As above, for the personal-data scanner restart
SensitiveContent:Pii:Categories:<n> string unset As above restart
SensitiveContent:Pii:Suppressions:<n>:Category string As above restart
SensitiveContent:Pii:Suppressions:<n>:Rule string As above restart
SensitiveContent:PersonalDataAnalyzer:Endpoint string unset Required once Pii is on, and an absolute http or https address; read by nothing while that switch is off restart
SensitiveContent:PersonalDataAnalyzer:Languages:<n> string unset Two lowercase letters each, naming a language the analyzer loads a model for and registers recognizers in; an absent list yields en. At most eight, since one scan asks once per language inside a single ScanTimeout. The order is not read — the set is deduplicated and ordered before use — and the set is part of the derivation stamp restart
SensitiveContent:PersonalDataAnalyzer:MinimumConfidence double 0.4 0 – 1 inclusive, compared inclusively by the analyzer. It decides which regions are replaced, so it is part of the derivation stamp and changing it marks earlier-derived rows stale restart
SensitiveContent:MaximumAnalyzedCharacters int 200000 1 – 10000000; text beyond it is dropped from the result rather than handed on unscanned. On the derived path that is what is stored, so lowering it truncates every message indexed afterwards and the value is part of the derivation stamp restart
SensitiveContent:ScanTimeout TimeSpan 00:00:15 One second to two minutes, per call to one scanner — which for the personal-data scanner covers every configured language together rather than each. A scan that misses it is refused rather than served unscanned, and on the derivation path that refusal ends the synchronization run carrying it, so a budget below what the analyzer spends on a large body leaves a folder repeating the same batch. It also bounds one personal-data readiness scrape whole, so naming more languages costs more analyzer requests and never a longer scrape restart
SensitiveContent:MaximumConcurrentScans int 4 1 – 256, across the process restart
SensitiveContent:RebuildStaleDerivedData bool false Read only while a scanner is on; re-derives every message whose derived text predates the current configuration restart

The rebuild switch spends a whole mailbox. Switching a scanner on, or widening what it looks for, protects what is derived from that moment onward and reaches nothing already extracted, chunked, or embedded — the host reports how many messages that leaves behind every time it starts. This key is what re-derives them, and it costs one full re-indexing of the affected messages: each is read, extracted, scanned, re-chunked, and re-embedded, so a deployment with a hosted embedding endpoint pays that provider again for every one. It rides the extraction backfill rather than a worker of its own, so MailExtractionBackfill:Enabled has to be on for it to perform anything, and MailExtractionBackfill's interval and batch size are what pace the spend. Switch it back off once the count reaches zero. Derived data records what is stamped, what makes a row stale, and why nothing rewrites stored text in place.

A category name is matched against what the scanner declares, ignoring capitalization, and the declared spelling is what survives the match — so a placeholder in redacted text does not depend on how the name was written here. A name that matches nothing fails startup and quotes both the value and the categories the scanner does detect, rather than being dropped by the binder and leaving the section reading as protection that is on. So does a suppression naming a rule that does not exist. A suppression inside a category this deployment does not look for is accepted and inert.

The Secrets scanner declares these seven categories. Six are on when Categories names none; the seventh is not, and listing categories replaces the default set, so switching the entropy heuristic on means naming every category wanted alongside it.

Category What it finds On by default
ProviderToken An API token, key, or session credential a named service issued and prefixes as its own yes
CloudAccessKey An access key or client secret for a cloud platform's own control plane yes
PrivateKey A private key or certificate bundle, armoured as PEM or encoded whole yes
JsonWebToken A JSON Web Token, in its ordinary form or encoded a second time yes
ConnectionString A connection string carrying the credential it connects with yes
CredentialUrl A URL carrying a credential in its user information, its path, or its query yes
HighEntropyString A string dense enough to be a credential, recognised by its randomness rather than its shape no

A rule name inside them is the corpus entry's own name — github-pat and aws-access-token from the gitleaks rule data, AzureCosmosDBIdentifiableKey and UrlCredentials from the detection engine's own corpus, and database-connection-uri-credential from MailFathom's. Sensitive-content scanning records where each corpus comes from and what the entropy heuristic costs.

The Pii scanner declares these eleven categories. The first five are on when Categories names none, and listing categories replaces that set, so adding a personal name means naming the five wanted alongside it.

Category What it finds On by default
PaymentCard A payment card number yes
BankAccount An IBAN or another bank account number yes
NationalIdentifier A national identification, social-security, or tax number yes
IdentityDocument A passport, identity-card, or driving-licence number yes
HealthIdentifier A number that names a person inside a health system yes
PersonName A personal name no
EmailAddress An email address no
PostalAddress A postal address, or a place named precisely enough to be one no
PhoneNumber A telephone number no
Date A date or a time, absolute or relative no
NetworkAddress An address that identifies a machine, whether the network assigned it or the hardware carries it no

A rule name inside them is the analyzer's own entity name, spelled as the analyzer spells it — CREDIT_CARD, IBAN_CODE, US_SSN, UK_NHS — which is what lets a suppression silence one recognizer inside a category that stays on. An operator never names one in Categories: those are the units this product publishes, and the mapping between the two is MailFathom's. The personal-data scanner records what each of the six optional categories costs retrieval, why the endpoint belongs inside the deployment, and what the confidence floor is protecting against.

The analyzer block is read only while Pii is on. An address left behind under a scanner nobody runs is accepted and inert, for the reason a category list under one is: it describes no protection, so refusing to start over it would be refusing over a comment. The reverse — the scanner on with no address, a relative or non-HTTP address, a language that is not two lowercase letters, more than eight of them, or a floor outside 0 to 1 — fails startup naming the key.

Every code in Languages is one the analyzer has to have been built for, and setting the key is only the last of the steps that make a language work: the model, the image, the analyzer's recognizer registry, its engine, and then this key. A code the analyzer registers nothing for leaves the deployment unready naming that code, rather than falling back to English or quietly contributing nothing. Which codes are named decides which of the eleven categories above can find anything — under pl alone the shipped registry knows one national identifier and no identity document at all, which is the reason to name the languages a mixed mailbox actually carries rather than choosing between them. The analyzer's languages records what each language reaches and what adding one takes.

Each language is a request, and they share one budget. A scan states one language per call, so a deployment naming two asks the analyzer twice over the same text, one after the other, and merges what came back; ScanTimeout bounds that whole scan rather than each call, and MaximumConcurrentScans still counts scans rather than requests. Two languages reporting the same value over the same span are one finding carrying the stronger score, and overlapping regions merge into one placeholder as they already did.

The readiness probe judges a category across the set. A category is reachable when at least one configured language recognises an entity of it, so adding a language never turns a ready deployment unready — while a category no configured language reaches still refuses the start, naming the category as it does today.

The analyzed ceiling defaults to the same number as EmailContent:MaxCharactersPerRead, so an ordinary content read is analyzed whole and only something pathological reaches it.

SpamClassification

Whether mail is classified as spam, and where. A root of its own for the same reason SensitiveContent is one: it is a property of the deployment rather than of one account, and what it switches on reaches the mailbox reads as well as the classification. Spam classification records what a classification holds, which facts the deterministic stage reads, and why a scanner never overturns a provider's own verdict.

Every switch is off by default, and an absent section is that default rather than a startup failure. The deterministic stage works alone and is the whole of the feature without a sidecar; UseScanner adds the Apache SpamAssassin daemon described below.

Key Type Default Constraint Change
SpamClassification:Enabled bool false reload
SpamClassification:UseScanner bool false Asking for a scanner while Enabled is false fails startup, because a scanner is only consulted where classification runs restart
SpamClassification:ScannedFolders:<n> string unset A usable folder alias; an absent list is every account's inbox mapping, and an explicitly empty list is no folder at all reload
SpamClassification:ScannerThreshold double unset 0.1 – 1000; unset keeps the threshold the scanner itself answered with reload
SpamClassification:ClassificationWait TimeSpan 00:15:00 1 s – 7 days; how long a stored message may wait for a verdict before it is derived from anyway reload
SpamClassification:RunBatchSize int 50 1 – 10 000 reload
SpamClassification:MaxRunBatchesPerPass int 4 1 – 1 000 reload
SpamClassification:Scanner:Host string unset Required once UseScanner is on, and a host name or IP address rather than a URL or an address with the port on it restart
SpamClassification:Scanner:Port int 783 1 – 65535 restart
SpamClassification:Scanner:ScanTimeoutSeconds int 30 1 – 120 restart
SpamClassification:Scanner:MaximumMessageBytes int 512000 32 000 – 33 554 432 restart
SpamClassification:Scanner:MaximumConcurrentScans int 5 1 – 64 restart
SpamClassification:Actions:MoveToJunkFolder bool false Asking for it while Enabled is false fails startup, and so does an account that maps no destination to file into reload
SpamClassification:Actions:MarkAsRead bool false Asking for it while Enabled is false fails startup reload
SpamClassification:Actions:JunkFolder string role:Junk A folder alias, or a role written as role:<name>; every configured account has to map it once filing is on reload
SpamClassification:Actions:Threshold double unset 0.1 – 1000; unset acts on every spam verdict, and a value judges what a scanner scored reload

UseScanner and the Scanner block are read once, at startup: whether a scanner exists at all decides what is constructed and whether the host refuses to start without a daemon, which a reload cannot revisit. Everything else in this section is read per classification.

ClassificationWait bounds the ordering rather than a scan. Wherever classification is on, a message it covers is not chunked, embedded, or offered to the rule set until a verdict exists — and this is how long that may hold before the message is derived from regardless, which is what stops a classifier nobody noticed was wedged from silently stopping the index. Lengthening it delays mail of a classified folder by that much longer in the worst case; shortening it narrows the window in which a verdict can arrive first. Zero is refused, because a wait of none releases every message before anything could have scored it. Junk is kept out of what a deployment derives from mail records what each answer means and what is counted.

A scanner switched on with no daemon answering fails startup, with error code 81003 naming the key to repair rather than the address it tried. That is deliberate asymmetry with what one message gets, where a failed scan leaves the deterministic verdict standing: an instance whose sidecar never came up would classify everything from headers alone and look healthy doing it. The bounds are validated whether or not the scanner is switched on, so a value written wrong is reported before the run that first switches scanning on rather than during it.

The daemon receives whole messages, so it belongs inside the deployment's own trust boundary; the feature page states what an address outside it gives up, and what the rule-update and DNS postures cost. The deployment assets carry the sidecar itself — Kubernetes, Compose, and Quadlet.

The default scope follows the folder role rather than the text INBOX: it is whichever alias each account maps to Inbox in MailSynchronization, so a server presenting the inbox under another name is classified without the scope being restated here. The two shapes of an unset list are deliberately distinguishable — writing no key asks for that default, and writing an empty list asks for no folder, which switches the work off without switching the section off.

A folder alias that this system could never have issued fails startup and names itself, rather than being dropped by the binder and leaving the section reading as a scope that is covered. So does a threshold outside the range above: one at or below zero files every message whatever a scanner answered, and one beyond the ceiling can never be reached, so both are a typed digit rather than an intent.

The section is read per classification rather than captured, so a reload takes effect on the next one. What a reload never does is revisit a message already classified: replacing a verdict is an explicit operation.

Which folder is left out of list_emails and search_emails is not configured here. It is the folder mapped to the Junk special use in MailSynchronization, and it is withheld whether or not this section switches anything on.

The Actions block is the only part of this section that writes to a mailbox, and both of its switches are off. Each works alone: filing moves the message on the server, marking read sets its \Seen flag, and turning both on sets the flag first, because a relocation can renumber the message. Nothing else is ever done — no delete, no other flag, no folder created, nothing sent.

JunkFolder does not have to be a folder MailFathom mirrors, and for most deployments it should not be: mapping it with synchronize: false files spam out of the instance entirely, under the account's own AuthoredDeleteEmailDisposition. What it does have to be is a folder every configured account maps, because classification asks for none to be created — an account that maps no destination fails startup naming that account, rather than leaving its spam unfiled with nothing said about why. A folder the account only maps is resolved against the server the first time a filing needs it, exactly as a rule's destination is.

RunBatchSize and MaxRunBatchesPerPass bound one pass of the classification run an operator asks for, and neither is a schedule: a pass is a step of the account's synchronization run, so how often one happens is that run's interval. What these decide is how much of a mailbox one pass takes in hand — raising them walks a mailbox nobody has scored in fewer account runs, and lowering them shortens the stretch an interrupted pass has to cover again and leaves more of each run for the folders it exists to fetch. Both defaults are smaller than the rule pass's, because a classification reads the stored message and, with a scanner configured, sends the whole of it across a socket and waits for a score.

Threshold judges what a scanner scored, in the scanner's own scale, so an operator can label at ScannerThreshold and move mail only from a higher score. It reaches no other stage, exactly as ScannerThreshold does not: a verdict resting on a provider's header or on where the receiving server filed the message carries no score in this scale, and is acted on. Raising it is deliberately not the same edit as switching classification off — the verdicts go on being recorded.

Embeddings

What this deployment intends to embed with. Writing nothing is a supported deployment: no vectors are produced, semantic search is unavailable, and lexical search serves exactly as before. Declaring a chain does not start spending — an activation does. Embedding generation records what a declaration means and what it costs.

Nothing here is a switch for semantic search, and none of these keys turns it on. What a search reports as its semantic capability follows from three facts this section does not hold: whether a profile has been activated, whether the declaration below still names that profile's identity, and whether the last call to the endpoint chain was answered. A search never fails because one of them is not true — it answers lexically and says which of the three states it is in. Email search states what each means for a caller and what an operator does about it. Editing a key in this section and restarting therefore changes what is embedded next, never what a search is currently able to do; only an activation does that.

Key Type Default Constraint Change
Embeddings:AllowTrimVectors bool false with it off, a declared dimension above 2000 is refused at startup; with it on, a wider answer is cut to the declared width and renormalized restart
Embeddings:MaxPassagesPerRequest int 64 1 – 2048; the batch bound, applied before the provider sees a request restart
Embeddings:RequestTimeout TimeSpan 00:01:00 positive; one request to one endpoint restart
Embeddings:MaxQueuedEmails int 1024 1 – 1000000; newly synchronized messages that may wait to be embedded at once, beyond which synchronization stops offering and the backfill reaches the rest restart

What an instance is willing to spend

The four keys below bound cost rather than correctness, and they are validated whether or not a chain is declared: passages are cut for every synchronized message on an instance that has chosen no provider, so a ceiling left unvalidated would be one already applying. None of them is part of an embedding profile — they decide how many vectors exist and never what one means, so moving any of them leaves every stored vector as comparable as it was. Embedding generation records what each bounds and why.

Key Type Default Constraint Change
Embeddings:MaxCharactersPerEmail int 200000 1000 – 10000000; how much of one message's extracted text is cut into passages. A message beyond it is bounded rather than refused — its opening is embedded and the length its text had is recorded on the message restart
Embeddings:MaxRequestsPerMinute int 0 0 – 100000; 0 paces nothing, which is the default. For a provider whose quota is stated per minute; a caller takes the next free slot and waits for it restart
Embeddings:MaxInputCharactersPerPeriod long 50000000 zero or positive; the characters one period may send a provider, counted as sent rather than as stored. 0 declares no ceiling at all, which is supported and means an enabled feature can produce a bill nobody agreed to restart
Embeddings:SpendPeriod TimeSpan 1.00:00:00 1 min – 31 days; the fixed window the ceiling is counted over, anchored at the Unix epoch so every restart places it identically restart

Reaching MaxInputCharactersPerPeriod pauses embedding until the period rolls over, and resumes without anybody acting; nothing is dropped, because a passage with no vector is what the backfill selects on. The ceiling binds to within one batch: a batch is admitted whenever anything at all is left and is then paid for whole, because weighing it against what remains would stall a deployment whose ceiling is smaller than one batch for ever.

The default is chosen to bind. Fifty million characters a day is roughly twelve million tokens and embeds something like sixteen thousand ordinary messages, so an instance keeping up with arriving mail never meets it and one working through a decade of archive is paced rather than surprised — raise it deliberately for an initial backfill, having seen the number.

Concurrency is not here. How many provider calls may be in flight at once is Resilience:AiProviderInvocation:ConcurrencyLimit, which is the one setting that owns that question; outbound resilience holds it, and a second limiter beside it would make two keys answer for one behaviour.

One endpoint — Embeddings:Endpoints:<n>

An ordered chain. Every entry declares the same geometry and reaches the same vector space, so a failing endpoint falls through to the next without changing what any stored vector means; startup refuses a chain whose entries disagree, naming both aliases and the property.

An entry declares any service reachable over the OpenAI wire protocol, not one of a fixed set: Provider, Model, and ModelVersion are what the profile records, while RoutedModelName — or Model where that is empty — is the string a request is routed on. Two rules bind the address and the credential, and one implementation applies them to this section and to Chat alike: an address is absolute HTTP or HTTPS, with a plain http one refused wherever the endpoint holds a credential, because the request would publish it to everything on the path; and exactly one of ApiKey, EntraCredential, and Unauthenticated is declared, because none of them is what a forgotten reference looks like and two leaves unsaid which one a request presents. Embedding generation § An endpoint is any service that speaks the OpenAI wire protocol holds both rules with their reasons, what each setting decides, and a worked example of an endpoint that is neither OpenAI nor Azure. Embedding generation § A model server you run yourself covers the plain-address case, what it gains, what it gives up, and the startup warning an instance holding such an endpoint writes. Provider endpoints is the register of services somebody checked — what each one's Address and credential are, whether it serves an embeddings route at all, and whether SupportsRequestedDimension may stay at its default.

Key Type Default Constraint Change
…:Alias string required, unique within the chain; what a log line, a metric tag, a resilience circuit, and a failure message call this endpoint restart
…:Provider string required, at most 64 characters; the vendor whose model defines the space, not the endpoint it is reached at restart
…:Model string required, at most 128 characters; the vendor's published model identifier restart
…:ModelVersion string (empty) at most 64 characters; empty is a vendor that versions nothing, which is the ordinary case restart
…:RoutedModelName string (empty) at most 128 characters; what is sent as the model of a request where that differs — a cloud deployment's own name. Empty means it equals Model restart
…:Dimension int 1 – 16000, and 1 – 2000 unless AllowTrimVectors is on; the width the stored vectors have and the profile records restart
…:DistanceMetric enum Cosine Cosine, InnerProduct, EuclideanDistance restart
…:InputCharacterLimit int 8000 positive; what a passage is cut to before it is sent, which is what the model saw and therefore part of what a vector means restart
…:PassageInstruction string (empty) at most 512 characters; empty for a model that requires none. Whitespace is refused, because it would register a second profile for a space identical to one already registered restart
…:NormalizeVectors bool true whether the space's vectors are of unit length restart
…:Address string (empty) absolute HTTP or HTTPS; a plain http one only for an endpoint declaring Unauthenticated. Empty uses the provider library's default. A cloud resource's OpenAI-compatible address ends in /openai/v1/ restart
…:SupportsRequestedDimension bool true whether the endpoint honours a requested width, so the narrower space is asked for rather than cut out of a wider answer restart
…:ApiKey secret block (absent) the provider key. Exactly one of this, EntraCredential, and Unauthenticated is declared restart, value read per request
…:Unauthenticated bool false that this endpoint asks for no credential, so a request presents none — the shape of a model server you run yourself. Written rather than inferred from the other two being absent, because that is what a forgotten key reference looks like restart

Microsoft Entra credential — Embeddings:Endpoints:<n>:EntraCredential

For an endpoint where no key exists to provision. All four shapes are non-interactive by construction: MailFathom is a background service with nobody at a keyboard, and DefaultAzureCredential is deliberately not used because its chain reaches an interactive browser credential and the developer-tool credentials of whoever is signed in on the host.

Key Type Default Constraint Change
…:Kind enum ManagedIdentity ManagedIdentity, WorkloadIdentity, ClientSecret, ClientCertificate. ApiKey and Unauthenticated are refused here; a key is declared as one, and an endpoint needing no credential says so on the endpoint restart
…:TokenScope string https://ai.azure.com/.default required; the audience an access token is minted for. Declared rather than derived from the address, so a renamed service does not silently mint tokens for the wrong audience restart
…:TenantId string (empty) required for ClientSecret and ClientCertificate restart
…:ClientId string (empty) required for ClientSecret and ClientCertificate; optional for ManagedIdentity, where it selects a user-assigned identity restart
…:ClientSecret secret block (absent) required for ClientSecret restart
…:CertificatePath string (empty) required for ClientCertificate; a PKCS#12 file the process account can read restart
…:CertificatePassword secret block (absent) where the certificate file has one restart

Chat

What this deployment generates text with. A root of its own beside Embeddings rather than a block inside it, because the two are separate choices with separate consequences: without an embedding provider semantic search is off and lexical search continues, while without a chat provider search is unaffected and only the answering capability stops being offered. Writing nothing is a supported deployment, exactly as writing no Embeddings section is. Chat generation records what a declaration means and what one call may spend.

One endpoint rather than an ordered chain. A fallback embedding endpoint is another route to one vector space and startup proves it; nothing proves that of two chat models, so falling through would answer a person in a different model's voice with nothing above able to tell. An operator who wants failover puts a gateway in front of the declared endpoint.

The endpoint is any service reachable over the OpenAI wire protocol, under the same two rules the embedding chain follows and through the same implementation of them: an absolute HTTP or HTTPS address with a plain http one refused wherever a credential is held, and exactly one of ApiKey, EntraCredential, and Unauthenticated. Chat generation § An endpoint is any service that speaks the OpenAI wire protocol carries a worked example of one that is neither OpenAI nor Azure, and Chat:Api is the key most often decided by which of the two paths such a service serves. Provider endpoints records which paths each checked service was found to serve.

Key Type Default Constraint Change
Chat:Alias string (empty) writing one is what configures a chat provider at all; unique across every AI endpoint the deployment declares, embedding endpoints included. A section carrying other settings without it is refused rather than ignored reload to rename, restart to declare or remove
Chat:Model string required once an alias is written; what a request is routed to, which for a cloud deployment is the deployment's own name rather than the vendor's model identifier reload
Chat:Address string (empty) absolute HTTP or HTTPS; a plain http one only for an endpoint declaring Chat:Unauthenticated. Empty uses the provider library's default. A cloud resource's OpenAI-compatible address ends in /openai/v1/ reload
Chat:Api enum ChatCompletions ChatCompletions or Responses; which of the provider's two request APIs a call goes to under the declared address. Declared rather than derived, because the routed model name is the operator's own deployment name and nothing about it says which paths the server serves. State Responses for a reasoning model that refuses function tools beside a stated effort; a server that does not serve that path answers request refused reload
Chat:MaxOutputTokens int 1024 1 – 200000; what one answer may occupy. Reaching it is not a failure — the answer arrives marked as cut short reload
Chat:Temperature float (unset) 0 – 2; left unset sends nothing, which is required by the models that reject the parameter outright reload
Chat:TopP float (unset) 0 – 1; unset the same way, and for the same reason reload
Chat:ReasoningEffort string (unset) the level the model documents, written as the provider spells it — none, minimal, low, medium, high, xhigh, or whatever a later model adds. Unset sends no reasoning parameter at all, which a model that does not reason requires. none is not the same as unset — it states an effort of none and sends it, which is what a provider refusing tools beside an unstated effort asks for. Startup checks the shape and not the vocabulary, because which levels exist belongs to the model; a level this deployment's model does not accept refuses the request rather than falling back reload
Chat:MaxMessagesPerRequest int 64 1 – 512; the turns one request carries, refused rather than truncated reload
Chat:MaxRequestCharacters int 120000 1 – 4000000; what those turns may add up to. Stated in characters rather than tokens because counting tokens would mean carrying the model's own tokenizer; set it below what the model's context window allows reload
Chat:RequestTimeout TimeSpan 00:02:00 positive; one request. Longer than an embedding request's by default, because generating an answer takes as long as the answer is reload
Chat:ApiKey secret block (absent) the provider key. Exactly one of this, EntraCredential, and Unauthenticated is declared reload, value read per request
Chat:Unauthenticated bool false that this endpoint asks for no credential, so a request presents none — the shape of a model server you run yourself. Written rather than inferred from the other two being absent, because that is what a forgotten key reference looks like reload

What a reload changes here, and what it does not. Everything the declared endpoint says is read again per question, so correcting a model the provider refused — the ordinary case, because a wrong model is only discovered from a refusal — costs an edit rather than a restart of a process that is synchronizing mailboxes and holding an IMAP IDLE connection. A run already in flight keeps the declaration it began with, so a reload landing mid-question changes the next question and not that one. A candidate that breaks any rule in the table is refused whole, logged with the key to fix, and leaves the previous declaration answering; the process stays up either way. What stays a restart is the pair that decides which services this deployment registered at all: whether Chat:Alias names an endpoint, and whether Chat:RelevanceFilter:Enabled turns the second pass on. Renaming a declared alias reloads, because the credential and the resilience circuit are both looked up by whatever the declaration in force calls it; going from no chat section to one, or the reverse, does not, and is refused with that message rather than silently ignored.

What the declared model has to be able to do. ask_mail answers by offering the model a retrieval tool and reading mail when the model calls it, so a model that cannot be given function tools cannot answer a question here whatever else is written above. That is what the two settings in the middle of the table exist for: a current reasoning model refuses function tools beside an unstated reasoning effort and names the responses API as the way to have both, so such a model needs Chat:Api set to Responses and Chat:ReasoningEffort written — including written as none, which states an effort rather than omitting the parameter. A model this deployment cannot use is not detected at startup, because nothing here can ask a provider what a routed name supports without paying for a call; it surfaces as request refused on the first question. Chat generation holds the whole reasoning, and Mail answering describes the run that imposes the requirement.

Microsoft Entra credential — Chat:EntraCredential

The same block, with the same keys, defaults, and rules as Embeddings:Endpoints:<n>:EntraCredential above. One credential source resolves both sections, which is why the alias uniqueness rule spans them. Its keys reload here and take a restart there, for the reason the table above gives: this section is read again per question and the embedding chain is read once while the host composes itself.

Relevance filter — Chat:RelevanceFilter

The optional second pass over a retrieval: each candidate the fused ranking produced is put to the declared chat endpoint on its own, and the ones the model scores below the threshold are dropped before an answer is written. A block inside Chat rather than a root of its own, because the pass judges with that endpoint and has nowhere to send a question without one — removing the chat section removes this with it.

Off by default, and off is a supported deployment: retrieval then hands over the fused ranking exactly as hybrid search produced it. Turning it on is a spend decision as much as a quality one — it costs one provider call per candidate on every lookup a question makes. Mail answering § An optional second pass describes what it drops, what it keeps, and what it does when the provider cannot tell it.

Key Type Default Constraint Change
Chat:RelevanceFilter:Enabled bool false turning it on requires a declared Chat:Alias, and a Chat:MaxMessagesPerRequest of at least 2, because a judgement is an instruction and a candidate restart
Chat:RelevanceFilter:MaxCandidates int (unset) 1 – MailAnswering:MaxPassagesPerRetrieval, which is everything one retrieval hands over; a higher value would name candidates that never exist and is refused at startup rather than accepted and never met. Unset judges every passage the retrieval hands over, which is why there is no literal default here: one would go on saying a number of its own after the retrieval it follows was narrowed or widened. The ceiling on what one lookup spends and how long it takes; set below what retrieval returns it buys a weaker filter rather than a shorter result, because a passage nobody judged keeps its place reload
Chat:RelevanceFilter:MinimumRelevance int 50 1 – 100, on the scale the model answers a judgement on. A threshold of 0 is refused: it would pay for a judgement that can drop nothing reload

MailAnswering

What answering one question is allowed to cost, and how much of a mailbox may leave the process to do it. A root of its own beside Chat rather than a block inside it, because the two answer different questions: Chat says which endpoint generates text and what one call to it may carry, while this bounds a run — the conversation in which a model looks mail up, reads it, and writes an answer — and the aggregate over every run of a period.

Unlike the provider sections, an absent section is not an absent capability. Every deployment has these ceilings and writing nothing takes the conservative defaults below, because an absent provider is a capability nobody asked for while an absent ceiling would be a bill nobody asked for. Mail answering § What one question may spend describes what each one does when it is reached, and what a caller is told.

The period is a fixed window anchored at the Unix epoch, placed exactly as Embeddings:SpendPeriod places the other spend ceiling: an AggregatePeriod of one hour begins on the hour, so a refused caller has a roll-over instant to come back at. A client that spends the whole allowance at the end of one window and again at the start of the next has therefore spent twice the ceiling across an interval of the same length.

Unlike the embedding ceiling, this ledger is process-local and not durable: a restart begins the current window with nothing spent. The difference is deliberate and is stated rather than implied — an embedding sweep charges inside a transaction that was committing vectors anyway, while answering opens no write of its own, so a durable count here would put a database write on the path of every provider call in every run.

Key Type Default Constraint Change
MailAnswering:MaxPassagesPerRetrieval int 20 1 – 50; how many messages one lookup may draw on. Capped by what one search can rank, because a retrieval is answered from a search window, and defaulted to the window search_emails itself returns so that asking a question reaches as many messages per lookup as searching for the same thing would restart
MailAnswering:MaxCharactersPerPassage int 1200 1 – 100000; how much of any single message a lookup may draw out. Separate from the count above, because one enormous extract and a spread across several messages say different things about a mailbox restart
MailAnswering:MaxRetrievedCharactersPerRun int 20000 1 – 10000000, and at least MaxCharactersPerPassage or no lookup could hand over even one passage. The ceiling on how much retrieved mail leaves the process to answer one question, whatever the model asks for, and the one that decides how many lookups a run fits: a lookup whose every passage reached the per-passage ceiling would exhaust a run on its own. Reaching it cuts rather than refuses: the run answers from what it has and the response says the mailbox was not read in full restart
MailAnswering:MaxProviderCallsPerRun int 8 1 – 1000; the ceiling that holds whatever the provider reports, because a run is a tool loop whose length is the model's decision. Reaching it stops the run with 57001. The run is also bounded by wall clock: raising this means raising McpEndpoint:RequestTimeout:Duration with it, or the extra calls are bought and then abandoned with a 504 restart
MailAnswering:MaxTokensPerRun long 80000 1 – 100000000; the cost ceiling, stated in the unit a provider bills by. Checked before each call against what the calls before it reported, so the call that crosses it is paid for — what a call will cost is not knowable until it is answered. Reaching it stops the run with 57001 restart
MailAnswering:MaxAnswerCharacters int 20000 1 – 1000000; how much of the model's answer one response carries. Cut rather than refused, and the response says it was cut restart
MailAnswering:MaxCitations int 20 1 – 1000; how many messages one response names. Cut the same way and reported the same way restart
MailAnswering:AggregatePeriod TimeSpan 01:00:00 positive; how long one period lasts before what was spent in it is forgotten. An hour rather than a day, because a ceiling an operator only meets once a day is one they meet after the spend has happened restart
MailAnswering:MaxRunsPerPeriod int 30 1 – 1000000; the ceiling on how enthusiastic a client may be. Nothing about the MCP surface stops one from asking a hundred questions in a minute, and without this a per-run ceiling bounds each of those hundred and none of the total. A question over it is refused with 57001 restart
MailAnswering:MaxTokensPerPeriod long 300000 1 – 10000000000; the same ceiling in what a provider bills. Checked before a run begins, against what the runs of this period have consumed so far restart

EmbeddingBackfill

The sweep that gives mail stored before the active profile its passages and its vectors. A root of its own rather than a block inside Embeddings, because what an instance embeds with is a commitment and how fast it works through the mail it already had is a rate an operator changes while watching a bill. Every key here is a pacing control: BatchSize × MaxBatchesPerRun is the most one run may spend, and Interval is how often that is paid. Embedding backfill describes what it reaches and why it repeats.

Key Type Default Constraint Change
EmbeddingBackfill:Enabled bool true turning it off stops the spending within one interval and loses nothing already embedded restart
EmbeddingBackfill:Interval TimeSpan 00:00:30 1 s – 24 h; the pause between runs while messages still await embedding restart
EmbeddingBackfill:IdleSweepInterval TimeSpan 00:15:00 1 s – 24 h; the pause before a sweep starts again after one reached the end restart
EmbeddingBackfill:BatchSize int 20 1 – 500 restart
EmbeddingBackfill:MaxBatchesPerRun int 5 1 – 1000 restart

MailExtractionBackfill

The worker that extracts text for messages stored before extraction existed or before a limit was raised.

Key Type Default Constraint Change
MailExtractionBackfill:Enabled bool true restart
MailExtractionBackfill:Interval TimeSpan 00:00:30 1 s – 1 h restart
MailExtractionBackfill:BatchSize int 50 1 – 500 restart
MailExtractionBackfill:MaxBatchesPerRun int 10 1 – 1000 restart