How a call works
Every IntentSharp call works the same way, whether it is AIAsync<T> on a value, a collection method, an intent run
from a file or a database query plan: your value and your intent go to the model, the answer is checked, and a
failed check goes back to the model with the findings, within limits you set. This page describes that call as your
code sees it. The Overview shows the same call as a picture, stage by stage.
A call returns a result, or throws before it starts
A call that returns an AIResult<T> and reaches the model returns its result. A wrong answer, a refusal, an error
from a provider and a limit that was hit all end up in the result, in Success and Issues; none of them throws.
Check Success before you read Value. Results and verification describes what the
result holds.
Such a call throws only for a mistake in the call itself, before any model call:
| Exception | When |
|---|---|
ArgumentException |
The intent is empty, the input is longer than MaxInputChars (200,000 characters by default), or the request names a provider the client does not have. |
ArgumentOutOfRangeException |
A per-request limit is out of range: MaxAttempts, MaxModelCalls or MaxOutputTokens below 1, Temperature outside 0 to 2, or a Timeout that is not greater than zero. |
InvalidOperationException |
A method without a client parameter is called outside client.AsAmbient(), so there is no client to use. |
IntentNotFoundException, IntentDisabledException, IntentShapeMismatchException, IntentSharpException |
An intent run from a file: the name is unknown, the file says enabled: false, the file's shape does not fit the type you ask for, or the file names a validator that is not registered. |
Input that is too long is refused, never cut. Two more things reach your code as exceptions: a cancellation through
your own CancellationToken, and an exception from a provider of your own that is not one of the IntentSharp
provider exceptions.
The database methods are different. QueryAsync and UpsertAsync return a QueryResult, not an AIResult<T>,
and throw SemanticDatabaseException when no safe statement results, including after model calls: when no attempt
passes the check, when the model declines, or when the statement is refused. They also throw it when the database
rejects the statement. See Database queries.
An attempt is one request and its checks
An attempt sends one request to the model and checks the answer. The checks run in your process, with no model
call: the answer must fit your type, and then your own rules run. When every check passes, the call ends with
Success true. When a check fails, the call makes another attempt, until it runs out of attempts.
Each attempt that finished its checks is recorded in Attempts, passed or not: the provider and the model that
answered, the tokens, the time, the reply text and the issues found with it.
Seeing every request lists the
attempts that have no record.
A failed check goes back to the model
The next attempt sends the same request with two additions: the previous reply and the findings, which are the messages of the checks that failed. The model reads your rule messages, so write them as instructions it can act on, such as "Quantity must be a positive number.". What is sent lists everything a retry carries.
Two kinds of failed attempt behave differently:
- A refusal ends the call at once, with no further attempt: asking again with the same input rarely helps.
- A reply cut off by the output token limit fails its attempt, and the next attempt tells the model so. If
answers are often cut off, raise
MaxOutputTokens.
Four limits stop a call
| Limit | Default | When it is reached |
|---|---|---|
MaxAttempts |
2 | The call ends with Success false and the findings of every attempt. |
MaxModelCalls |
6 | Attempts and judge calls together. The call ends with Success false and an issue that names the limit. |
MaxOutputTokens |
4,096 per model call | The reply is cut off and that attempt fails. |
Timeout |
none | The whole call, every attempt included. The call ends with Success false and an issue that names the time budget. |
Set the defaults for every call on IntentClientOptions (DefaultTimeout for the time budget), and override them
for one call on AIRequestOptions. The limits count calls, tokens and time, not money. Here a call allows five
attempts but only three model calls, and its rule fails every time:
var options = new AIRequestOptions { MaxAttempts = 5, MaxModelCalls = 3, Timeout = TimeSpan.FromSeconds(10) }
.Require<PurchaseRequest>(r => r.Quantity > 0, "Quantity must be a positive number.");
var order = await email.AIAsync<PurchaseRequest>(client, "Extract the purchase request from this email.", options);
Scripted model answer (every attempt): {"value": {"contact": "Dana", "sku": "A-1192", "quantity": 0, "deliverBy": "2026-11-15"}}
Success: false, after 3 attempts
Issues:
[Validator] attempt 1: Quantity must be a positive number.
[Validator] attempt 2: Quantity must be a positive number.
[Validator] attempt 3: Quantity must be a positive number.
[Guardrail] attempt 4: Model call ceiling of 3 reached before a verified result was produced.
Model calls (ScriptedProvider.CallCount): 3 of the 5 answers queued
The answers in this output come from a scripted model, not a real one.
A fallback provider answers only transient errors
With a fallback provider, a request that fails on the primary with a transient error goes to the fallback: an
HTTP 429, a 5xx, a transport error, a reply that cannot be read, a provider's HTTP timeout (not the call's time
budget), or your rate limiter making it wait longer than 30 seconds. Authentication errors, validation errors and
answers that fail a check never fall back. A request that names its provider turns fallback off for that request,
and so does FallbackOnTransientErrors set to false. A model named on the request is sent to the fallback too, so
name one both providers accept. Before an error counts as a failure, the built-in OpenAI and Anthropic providers
retry a 429, a 5xx or a transport error themselves, up to three times by default.
Providers covers both.
A cache hit makes no model call
With a request cache on the client (WithCache), an identical request is answered from the cache. Only the first
attempt of a call reads the cache, and a hit:
- makes no model call, is not counted in
MaxModelCallsand adds no tokens toUsage; - sets
FromCacheon the result; - is checked against this call's rules like a fresh answer. If it fails them, it is set aside and the model is asked instead, and the fresh answer replaces the cached one;
- still runs the judge when the judge is on for the call, and the judge is one model call.
A refusal or a reply cut off by the token limit is never cached. The built-in cache, MemoryRequestCache, lives in
your process, and an entry lasts one hour by default (CacheTtl). Memory, cache and rate limiting covers
the options.
Some calls need no model at all
None of these asks a model, and none is billed by your provider:
- A cache hit, as above.
- A call that throws before it starts, from the table at the top of this page.
- The checks themselves, your rules and code checks included. Only the optional judge asks a model.
- Loading and reloading intent files.
- Building agent tools from intent files: the model is asked only when a tool runs.
- Running a database query plan you already hold (
ExecutePlanAsync): the statement is checked again and run, with no model call. See Database queries. - Any call on
ScriptedProvider, which answers in your process from replies you script. See Testing.
After the call
Each model call that returns a reply adds its tokens and one call to IntentClient.TotalUsage and
TotalModelCalls, the client's running totals, as it returns, so the model calls made before an exception count
too. A model call that throws adds nothing. TotalModelCalls also counts EmbedAsync calls.
When the call ends, passed or failed:
- the call is recorded in the client's memory, unless the client has none,
IntentClientOptions.RecordInMemoryis off, the request setsDoNotRecord, or the answer came from the cache. The default memory keeps up to 10,000 entries in your process; - advice about the intent text, for example that it is very short, is in
Warnings. It never changesSuccess.
Each call also starts an Activity named IntentSharp.Transform for your tracing, and writes log entries to the
logger you give the builder. Logging and telemetry lists them.
Next
- What is sent: everything a call sends to your model provider.
- Results and verification: what a result holds and what each check means.
- Typed output: which types get a strict schema, and the options that shape an answer.