# IModelProvider

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/api/tracon.imodelprovider/
> Summary: IModelProvider: A model provider. Each provider is implemented in its own package (for example, Tracon.OpenAI) and registered with DI.

<div class="api-provenance">Namespace Tracon · Assembly <code>Tracon.Abstractions.dll</code></div>

A model provider. Each provider is implemented in its own package (for
example, `Tracon.OpenAI`) and registered with DI.

```csharp
public interface IModelProvider
```

## Remarks

This abstraction ensures that adding a new provider is *not a breaking
change*. This is the main reason for the modular packaging decision.

A provider outside the shipped packages is registered with
`AddModelProvider`. The runtime contract below is what the registry
and the compile path actually rely on; an implementation that breaks any
of it compiles and passes its own unit tests, then misbehaves only under a
real deployment. The
`Tracon.Testing.Contracts.Xunit` package ships
`ModelProviderContract`, which asserts the parts of this contract that
can be checked from outside; deriving it is the cheapest way to prove an
implementation honors them.

**Lifetime and threading**

An implementation is used as a **singleton**.
`AddModelProvider` registers it with
`AddSingleton`, and `ModelProviderRegistry` copies the registered
providers into a lookup *once*, when it is constructed. Two
consequences follow. A provider must not capture a scoped service — it
would be captured by a singleton and outlive its scope. And the set of
providers is fixed at startup: a provider cannot be added after the
container is built.

[IModelProvider.CreateChatClient](/api/tracon.imodelprovider/#Tracon_IModelProvider_CreateChatClient_Tracon_ModelBinding_) is called **concurrently** on
the same instance and must be thread-safe. Any per-credential client cache
an implementation keeps is therefore a concurrent one; note that
`ConcurrentDictionary.GetOrAdd`
may run its factory **more than once** for the same key when
two threads race, and discard the extra results. A factory that builds a
client must therefore be side-effect free and idempotent: building it twice
must be harmless.

**Naming**

[IModelProvider.Name](/api/tracon.imodelprovider/#Tracon_IModelProvider_Name) is matched against [ModelBinding.Provider](/api/tracon.modelbinding/#Tracon_ModelBinding_Provider)
with `StringComparer.OrdinalIgnoreCase`. Registering two
providers under the same name is not last-one-wins: the registry throws
[TraconException](/api/tracon.traconexception/) while it is being constructed, so the
host fails at startup rather than silently routing to one of them.

**The model catalog is metadata, not an allow list**

[IModelProvider.Models](/api/tracon.imodelprovider/#Tracon_IModelProvider_Models) drives the console's model picker, capability hints,
and cost reporting. It does **not** gate which models may be
used: an implementation is not required to reject a
[ModelBinding.Model](/api/tracon.modelbinding/#Tracon_ModelBinding_Model) that is absent from the catalog, and the
shipped providers do not — they log at most an informational message, so a
newly published model works without a new Tracon release. An empty
catalog is a normal, supported state.

The flags on [ModelDescriptor](/api/tracon.modeldescriptor/) do not carry equal weight.
[ModelDescriptor.SupportsStructuredOutput](/api/tracon.modeldescriptor/#Tracon_ModelDescriptor_SupportsStructuredOutput) is a real gate:
compilation of an agent that requests JSON output fails when the model is
*found* in the catalog and the flag is explicitly
[false](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/bool) (a model absent from the catalog is not checked).
[ModelDescriptor.SupportsTools](/api/tracon.modeldescriptor/#Tracon_ModelDescriptor_SupportsTools),
[ModelDescriptor.SupportsStreaming](/api/tracon.modeldescriptor/#Tracon_ModelDescriptor_SupportsStreaming) and
[ModelDescriptor.SupportsReasoning](/api/tracon.modeldescriptor/#Tracon_ModelDescriptor_SupportsReasoning) are advisory metadata that
nothing enforces at run time.

**Failures**

There is no required exception type. Tracon classifies a model-call
failure by the exception's type *name* and message *text*
across the whole exception graph, because `Tracon.Core` holds no
compile-time reference to any provider SDK's exception types. What that
means for an implementation: a failure whose message carries
`HTTP 429`, `too many requests`, `rate limit` or
`HTTP 5xx` lets [ModelBinding.Fallbacks](/api/tracon.modelbinding/#Tracon_ModelBinding_Fallbacks) move to the next
link; `HTTP 401` and `HTTP 403` deliberately do not (switching
providers would hide a configuration mistake); an
[OperationCanceledException](https://learn.microsoft.com/dotnet/api/system.operationcanceledexception) anywhere in the graph never
retries; and an **unrecognized** failure does not retry
either, because the retry set is closed and positive.

A response the provider filtered for safety is **not** an
exception at this layer. The correct signal is an ordinary
`AI.ChatResponse` carrying
`ChatFinishReason.ContentFilter`; the
registry's outermost ring turns that into
`TraconContentFilteredException`. Throwing instead makes the
circuit breaker count a healthy provider as failing.

A response the model stopped at its output token limit is not an exception
either. Report `ChatFinishReason.Length`
on the response (or on the last streamed update): the same ring fails a
structured (JSON) request with [TraconOutputTruncatedException](/api/tracon.traconoutputtruncatedexception/)
and lets a plain-text answer through with a
[RunEventCustomTypes.OutputTruncated](/api/tracon.runeventcustomtypes/#Tracon_RunEventCustomTypes_OutputTruncated) run event. An answer the
model stopped because the conversation filled its context window is the same
kind of cut: report [TraconFinishReasons.ContextWindowExceeded](/api/tracon.traconfinishreasons/#Tracon_TraconFinishReasons_ContextWindowExceeded), so the
error and the event name the window instead of the output limit.

A [TraconException](/api/tracon.traconexception/) thrown from
[IModelProvider.CreateChatClient](/api/tracon.imodelprovider/#Tracon_IModelProvider_CreateChatClient_Tracon_ModelBinding_) is wrapped as a compilation error naming the
agent; every other exception propagates raw. Use it for a configuration or
binding problem the host author can act on.

## Properties

### <a id="Tracon_IModelProvider_Models"></a> Models

The models this provider offers.

```csharp
IReadOnlyList<ModelDescriptor> Models { get; }
```

#### Property Value

 [IReadOnlyList](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist\-1)<[ModelDescriptor](/api/tracon.modeldescriptor/)\>

#### Remarks

Metadata, not an allow list — see the remarks on
[IModelProvider](/api/tracon.imodelprovider/). This is read on the compile path and may
be read concurrently; return a stable, immutable collection rather than
one that is mutated after construction.

### <a id="Tracon_IModelProvider_Name"></a> Name

The provider name. [ModelBinding.Provider](/api/tracon.modelbinding/#Tracon_ModelBinding_Provider) matches this
value. Comparison is case-insensitive.

```csharp
string Name { get; }
```

#### Property Value

 [string](https://learn.microsoft.com/dotnet/api/system.string)

## Methods

### <a id="Tracon_IModelProvider_CreateChatClient_Tracon_ModelBinding_"></a> CreateChatClient\(ModelBinding\)

Produces a **raw** chat client for the given binding.

```csharp
IChatClient CreateChatClient(ModelBinding binding)
```

#### Parameters

`binding` [ModelBinding](/api/tracon.modelbinding/)

The model binding.

#### Returns

 IChatClient

The provider-specific client. Decorators *specific* to the
provider (example: Anthropic's settings decorator) may be added here.

#### Remarks

**Do not build the common pipeline here.**
`UseFunctionInvocation`, `UseOpenTelemetry`, the content
guard, the circuit breaker, the per-provider concurrency limiter,
attachment resolution, response caching, the fallback chain and
content-filter detection are all added by
`ModelProviderRegistry.CreateChatClient`. Before that shared pipeline, every
provider package built the tool-call loop inside itself; the result
was that no ring the registry wraps around could see the loop's turns
— a tool result entered the model uninspected.

If the loop is also built here, two nested `FunctionInvokingChatClient`
instances form: the inner one resolves tools, the outer one never sees
any call. This is **not** merely cosmetic. The registry
places the content guard directly above the client this method returns,
so that every turn of the tool-call loop is inspected. With an inner
loop, the turn that carries a tool result back into the model runs
*beneath* the guard: measured on a single-tool run, the guard
sees the tool result only on its way out
(`ContentGuardDirection.Output`) and never on its way in
(`ContentGuardDirection.Input`) — which is the exact path prompt
injection takes. The reply text and the tool-call count are unchanged,
so nothing else reveals the mistake.

**Lifetime of the returned client.** Tracon does
**not** dispose it. The client is built once per compiled
agent and stored in it; the compiled agent
(`Microsoft.Agents.AI.ChatClientAgent`) implements neither
[IDisposable](https://learn.microsoft.com/dotnet/api/system.idisposable) nor [IAsyncDisposable](https://learn.microsoft.com/dotnet/api/system.iasyncdisposable), and
evicting an agent from the compile cache drops the reference without
disposing. An implementation therefore owns the lifetime of whatever it
returns, and what it returns must tolerate never being disposed. This
is why the shipped providers return clients backed by a long-lived,
shared SDK client rather than a per-call one: returning a client that
holds a resource needing release would leak it.
