Compendium.Adapters.Mistral
1.1.0-preview.6
dotnet add package Compendium.Adapters.Mistral --version 1.1.0-preview.6
NuGet\Install-Package Compendium.Adapters.Mistral -Version 1.1.0-preview.6
<PackageReference Include="Compendium.Adapters.Mistral" Version="1.1.0-preview.6" />
<PackageVersion Include="Compendium.Adapters.Mistral" Version="1.1.0-preview.6" />
<PackageReference Include="Compendium.Adapters.Mistral" />
paket add Compendium.Adapters.Mistral --version 1.1.0-preview.6
#r "nuget: Compendium.Adapters.Mistral, 1.1.0-preview.6"
#:package Compendium.Adapters.Mistral@1.1.0-preview.6
#addin nuget:?package=Compendium.Adapters.Mistral&version=1.1.0-preview.6&prerelease
#tool nuget:?package=Compendium.Adapters.Mistral&version=1.1.0-preview.6&prerelease
Compendium.Adapters.Mistral
Direct adapter for the Mistral AI "la Plateforme" REST API (https://api.mistral.ai/v1) that implements IAIProvider from Compendium.Abstractions.AI.
| What | Value |
|---|---|
| Target framework | net9.0;net10.0 |
| Abstractions pin | Compendium.Abstractions.AI 1.0.1 |
| HTTP stack | Typed HttpClient + Microsoft.Extensions.Http.Resilience (standard pipeline) + System.Text.Json |
| Auth | Authorization: Bearer <ApiKey> |
| Surface | Chat completions (sync + SSE streaming), embeddings (batched), tool calling, structured outputs, Pixtral vision |
| Tests | xUnit 2.9 + FluentAssertions + NSubstitute + RichardSzalay.MockHttp |
EU data residency — the headline value prop
Mistral AI hosts inference in France (EU). Payloads, including PII, never leave the EU. For European tenants subject to GDPR (or contractually required to keep data on EU soil), this adapter is the strongest default in the Compendium ecosystem. Pair it with Compendium.Adapters.OpenAI only when you need OpenAI-specific models.
| Need | Pick |
|---|---|
| GDPR / EU data residency | Compendium.Adapters.Mistral |
OpenAI-only models (e.g. o1) |
Compendium.Adapters.OpenAI |
| Gateway / model routing | Compendium.Adapters.OpenRouter |
| EU + Azure compliance overlay | Compendium.Adapters.AzureOpenAI (EU region) |
Installation
dotnet add package Compendium.Adapters.Mistral
Configuration
Register at startup:
services.AddCompendiumMistral(opt =>
{
opt.ApiKey = builder.Configuration["Mistral:ApiKey"]!;
opt.DefaultModel = "mistral-large-latest";
opt.DefaultEmbeddingModel = "mistral-embed";
});
…or bind from IConfiguration (section Mistral):
services.AddCompendiumMistral(builder.Configuration);
{
"Mistral": {
"ApiKey": "...",
"DefaultModel": "mistral-large-latest",
"DefaultEmbeddingModel": "mistral-embed",
"TimeoutSeconds": 120,
"MaxEmbeddingsBatchSize": 512
}
}
The standard resilience pipeline (AddStandardResilienceHandler) is applied automatically; tune retry / timeout behaviour by configuring MistralOptions.TimeoutSeconds and RetryAttempts.
Model catalog
| Model | Use case |
|---|---|
mistral-large-latest |
Top-tier reasoning, default chat model |
mistral-small-latest |
Cheap reasoning, integration tests |
mistral-medium-latest |
Balanced cost/quality |
open-mistral-7b, open-mixtral-8x7b |
Open-weight tiers |
codestral-latest |
Code-specialised (chat endpoint, same wire format) |
pixtral-12b-latest, pixtral-large-latest |
Vision (multimodal text + image) |
mistral-embed |
Embeddings (1024-dim) |
zai-glm-5-3 (zai-glm-5, zai-glm-latest) |
GLM 5.3, Z.ai's open-weight model hosted by Mistral — always reasons; see below |
GLM (zai-glm-*)
The GLM models Mistral hosts go through the GLM dialect (Compendium.Adapters.ZAi.Glm, shared with the Z.ai and
OpenRouter adapters — docs/adr/0001-glm-dialect.md):
- their content is always a list of
thinkingandtextchunks, in a stream too: the thinking goes toReasoningDelta/ReasoningwhenCompletionRequest.Reasoningasks for it, never into the answer; reasoning_effortisloworhighfor GLM 5.3 —Low→low;Medium,High,XHighandMax→high— andhighorxhighfor GLM 5.2 (fromReasoningOptions.Effort; none when no effort is asked). Mistral's reasoning guide listsmaxforzai-glm-5-3, but its API'sReasoningEffortenum (none…xhigh) does not, somaxis not sent. Nothinkingfield is sent (Mistral has none);max_tokensgets room for the reasoning on top of the answer's, with or withoutReasoning(GLM 5.3 reasons anyway): the caller'sBudgetTokens, else room for the effort actually sent — 4 096 forlow, 16 384 forhigh, 32 768 forxhighor when no effort is sent;ListModelsAsyncfills their context window (1M), output limit (128K) andMetadata["reasoning"] = "always".
Every other model is sent and read exactly as before.
Usage
Chat (sync)
var provider = sp.GetRequiredService<IAIProvider>();
var result = await provider.CompleteAsync(new CompletionRequest
{
Model = "mistral-large-latest",
SystemPrompt = "Be concise.",
Messages = new List<Message> { Message.User("Pick a colour.") },
MaxTokens = 64
});
if (result.IsSuccess) Console.WriteLine(result.Value.Content);
Chat (streaming)
await foreach (var chunk in provider.StreamCompleteAsync(request))
{
if (chunk.IsSuccess) Console.Write(chunk.Value.ContentDelta);
}
The final chunk carries IsFinal=true, FinishReason, and Usage (token counts).
Codestral (code-specialised chat)
Codestral exposes the same /v1/chat/completions surface as the general chat models. Switch by setting DefaultModel:
services.AddCompendiumMistral(opt =>
{
opt.ApiKey = "...";
opt.DefaultModel = "codestral-latest";
});
ListModelsAsync marks Codestral with AIModel.Metadata["specialty"] = "code" so callers can route automatically.
Embeddings (batched)
EmbedAsync chunks the input list into requests no larger than MaxEmbeddingsBatchSize (default 512) and aggregates the embeddings, re-mapping indices to the original input order.
var result = await provider.EmbedAsync(new EmbeddingRequest
{
Model = "mistral-embed",
Inputs = manyTexts
});
Tool calling
using Compendium.Adapters.Mistral.Tools;
using Compendium.Abstractions.AI.Agents.Models;
var tools = new List<AgentTool>
{
new(
Name: "get_weather",
Description: "Returns the current weather for the named city.",
InputSchemaJson: """{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}""")
};
var request = new CompletionRequest
{
Model = "mistral-large-latest",
Messages = new List<Message> { Message.User("Weather in Paris?") }
}.WithTools(tools, toolChoice: "auto"); // "auto" | "any" | "none" | <tool-name>
var result = await provider.CompleteAsync(request);
foreach (var call in result.Value.GetToolCalls())
{
Console.WriteLine($"{call.ToolName}({call.ArgumentsJson})");
}
Tool definitions are serialised into Mistral's tools[] array; the assistant's tool_calls are surfaced as IReadOnlyList<AgentToolInvocation> via CompletionResponse.GetToolCalls().
Native tool calls, with their ids, in a stream too
The calls also come back as the abstraction's ToolCall (Id, Name, ArgumentsJson): in CompletionResponse.ToolCalls,
and on the final chunk of a stream (CompletionChunk.ToolCalls, with FinishReason.ToolCall). A stream's tool_calls
deltas are put together: a call whole in one delta (what Mistral usually sends) or in pieces, several calls, arguments
as a string or an object. A call that comes with no id (absent, or the schema's default "null") is told apart from the
one before it by another tool's name, or by arguments that open an object once the first call's are whole JSON (with
no name, the new call is the tool of the last header); a header sent again continues the call. It gets an id from the
adapter: nine letters and digits (the format Mistral's validator requires), derived from the completion's id, the
call's place, its name and its arguments, so the same response, read again, gives the same id (a completion with no id
gets a random part instead). A stream that reaches [DONE] without a finish_reason still ends with the calls, on a
final chunk of its own. One cut before [DONE] while calls were under way, or cut by a read error (the connection
dropped), ends with AI.StreamInterrupted (the calls may be partial); a cancelled one ends with nothing more. To
continue, send the turn back, then one result per call:
conversation.Add(Message.Assistant(text, calls)); // → {"role":"assistant","content":…,"tool_calls":[…]}
conversation.Add(Message.ToolResult(calls[0].Id, resultJson)); // → {"role":"tool","content":…,"name":…,"tool_call_id":…}
Mistral may call several tools in one turn, and each call needs its own result. To get one call at most per turn, add
.WithParallelToolCalls(false) (Mistral's parallel_tool_calls); without it, the field is not sent.
The tool message's name is the called tool's, taken from the call in the conversation unless the message sets Name.
Send the ids back exactly as Mistral gave them (nine letters and digits, such as D681PevKs in its guide): an id it
did not give can be refused. A conversation without these fields is sent exactly as before.
Mistral specifics: Mistral uses
"any"(not"required") to force the model to emit a tool call. The adapter passes the value verbatim.
Structured outputs
using Compendium.Adapters.Mistral.StructuredOutputs;
var schema = """
{
"type":"object",
"properties":{ "city": {"type":"string"}, "tempC": {"type":"number"} },
"required":["city","tempC"],
"additionalProperties": false
}
""";
var request = new CompletionRequest
{
Model = "mistral-large-latest",
Messages = new List<Message> { Message.User("Weather in Paris as JSON.") }
}.WithStructuredOutput(schema, schemaName: "Weather");
// or: plain json_object mode (no schema)
request = request.WithJsonMode();
Globally opt every request into response_format: { type: "json_object" } with MistralOptions.UseStructuredOutputsByDefault = true.
The provider-agnostic way is CompletionRequest.ResponseSchema (new ResponseSchema { Name = "Weather", SchemaJson = schema }):
the same response_format.json_schema here, the same request on every adapter, and IAIProvider.SupportsJsonSchema(model) to ask first. It
takes precedence over the keys above. See the repository README, "Structured output".
Vision (Pixtral)
using Compendium.Adapters.Mistral.Vision;
var request = new CompletionRequest
{
Model = "pixtral-12b-latest",
Messages = new List<Message> { Message.User("Describe this image.") }
}.WithImages("https://example.com/cat.jpg"); // or a data:image/...;base64,... URL
var result = await provider.CompleteAsync(request);
WithImages(...) promotes the last user message into a Pixtral-style multimodal content-parts array ([{type:"text", text}, {type:"image_url", image_url:{url}}]). If no user message exists, a fresh one is appended.
Cancellation
Every async surface accepts a CancellationToken. The adapter:
- re-throws
OperationCanceledExceptionwhen the caller cancelled, so callers canawaitcleanly; - maps non-caller cancellations (timeouts) into
Result.Failure(AIErrors.Timeout(...)); - aborts streaming loops on cancellation between SSE chunks.
Error mapping
| HTTP | Error.Code |
|---|---|
| 401 | AI.InvalidApiKey |
| 402 | AI.InsufficientCredits |
| 404 | AI.ModelNotFound |
| 429 | AI.RateLimitExceeded |
| 5xx / other | AI.ProviderError |
| Timeout (non-cancellation) | AI.Timeout |
Mistral returns two error envelope shapes and the adapter parses both:
// OpenAI-compat shape:
{ "error": { "message": "...", "code": "...", "type": "..." } }
// Native Mistral shape (most common from the platform today):
{ "message": "..." | { "detail": "..." }, "type": "...", "code": "..." }
Observability
MistralOptions.EnableLogging = true emits the raw request/response bodies at Debug level via the injected ILogger<MistralHttpClient>. Keep it off in production — payloads may contain user PII (the very thing you picked Mistral for keeping inside the EU).
Production checklist
- EU region:
https://api.mistral.airesolves to a French (eu-west) endpoint by default — TLS handled by the runtime. Document this in your tenant DPA. - API key rotation: store
Mistral:ApiKeyin your Compendium secret store; rotate quarterly. Mistral supports listing+revoking keys via the platform UI. - Resilience:
AddStandardResilienceHandlercovers retries, jitter, and total-attempts budget. IncreaseTimeoutSecondsonly after confirming Mistral isn't rate-limiting you. - PII: leave
EnableLogging = falsein production unless debugging a specific tenant under a DPA carve-out. - Pricing:
mistral-large-latestis the priciest tier — gate it behind a tenant feature flag and fall back tomistral-small-latestfor non-premium workloads.
Sample
samples/01-eu-chat-with-pixtral wires up a Pixtral vision call and prints the EU-hosted reply. Run with MISTRAL_API_KEY=... dotnet run --project samples/01-eu-chat-with-pixtral.
Integration tests
Live tests live in tests/Integration/Compendium.Adapters.Mistral.IntegrationTests. They:
- skip automatically when
MISTRAL_API_KEYis unset; - are excluded from the default CI run via
--filter "FullyQualifiedName!~IntegrationTests"; - hit
mistral-small-latest/mistral-embedso they cost cents per full run.
export MISTRAL_API_KEY=...
dotnet test -c Release --filter "FullyQualifiedName~IntegrationTests"
SDK choice (ADR)
Decision: hand-rolled HttpClient + System.Text.Json, no Mistral official SDK dependency.
Reasons:
- Mirrors the proven
Compendium.Adapters.OpenAIandCompendium.Adapters.Deepseekpatterns. - Lets the framework's pinned
Microsoft.Extensions.Http.Resiliencepipeline handle retries / circuit breaking. - Mistral's wire format is small, stable, and OpenAI-compat at
/v1/chat/completionsand/v1/embeddings— the only real divergence is the dual error envelope, which we handle inline.
Out of scope (first preview)
- FIM (fill-in-the-middle)
/v1/fim/completions— Codestral-specific, separate API shape; deferred to v1.1. - Agents API (
/v1/agents) — Mistral's bespoke agent orchestration; deferred to v1.1. - Custom embeddings
dimensionsparameter — not exposed byCompendium.Abstractions.AI1.0.1'sEmbeddingRequest. Will follow when the abstraction adds it.
License
MIT — same as Compendium itself.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Compendium.Abstractions.AI (>= 1.1.0-preview.6)
- Compendium.Adapters.ZAi.Glm (>= 1.1.0-preview.6)
- Compendium.Core (>= 1.0.5-preview.5)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.13)
- Microsoft.Extensions.DependencyInjection (>= 10.0.13)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Http (>= 10.0.13)
- Microsoft.Extensions.Http.Resilience (>= 10.6.0)
- Microsoft.Extensions.Logging (>= 10.0.13)
- Microsoft.Extensions.Options (>= 10.0.13)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.13)
-
net9.0
- Compendium.Abstractions.AI (>= 1.1.0-preview.6)
- Compendium.Adapters.ZAi.Glm (>= 1.1.0-preview.6)
- Compendium.Core (>= 1.0.5-preview.5)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.20)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.20)
- Microsoft.Extensions.DependencyInjection (>= 9.0.20)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.20)
- Microsoft.Extensions.Http (>= 9.0.20)
- Microsoft.Extensions.Http.Resilience (>= 10.6.0)
- Microsoft.Extensions.Logging (>= 9.0.20)
- Microsoft.Extensions.Options (>= 9.0.20)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.20)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0-preview.6 | 574 | 10/31/2026 |
| 1.0.0-preview.0 | 124 | 5/21/2026 |