# ModelProviderContract

> 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.testing.contracts.providers.modelprovidercontract/
> Summary: ModelProviderContract: Behavior tests for the IModelProvider contract.

<div class="api-provenance">Namespace Tracon.Testing.Contracts.Providers · Assembly <code>Tracon.Testing.Contracts.Xunit.dll</code></div>

Behavior tests for the [IModelProvider](/api/tracon.imodelprovider/) contract.

```csharp
public abstract class ModelProviderContract : IAsyncLifetime, IAsyncDisposable
```

<dl class="api-relations"><dt>Inheritance</dt><dd><a href="https://learn.microsoft.com/dotnet/api/system.object">object</a> ← <a href="/api/tracon.testing.contracts.providers.modelprovidercontract/">ModelProviderContract</a></dd><dt>Derived</dt><dd><a href="/api/tracon.testing.contracts.providers.modelprovidercredentialcontract/">ModelProviderCredentialContract</a>, <a href="/api/tracon.testing.contracts.providers.modelprovidersettingscontract/">ModelProviderSettingsContract</a></dd><dt>Implements</dt><dd>IAsyncLifetime, <a href="https://learn.microsoft.com/dotnet/api/system.iasyncdisposable">IAsyncDisposable</a></dd><dt>Inherited Members</dt><dd><a href="https://learn.microsoft.com/dotnet/api/system.object.gettype">object.GetType()</a>, <a href="https://learn.microsoft.com/dotnet/api/system.object.memberwiseclone">object.MemberwiseClone()</a>, <a href="https://learn.microsoft.com/dotnet/api/system.object.tostring">object.ToString()</a>, <a href="https://learn.microsoft.com/dotnet/api/system.object.equals#system-object-equals(system-object)">object.Equals(object?)</a>, <a href="https://learn.microsoft.com/dotnet/api/system.object.equals#system-object-equals(system-object-system-object)">object.Equals(object?, object?)</a>, <a href="https://learn.microsoft.com/dotnet/api/system.object.referenceequals">object.ReferenceEquals(object?, object?)</a>, <a href="https://learn.microsoft.com/dotnet/api/system.object.gethashcode">object.GetHashCode()</a></dd></dl>

## Remarks

A provider Tracon does not ship a package for is registered with
`AddModelProvider` and then carries the same runtime obligations the
four shipped providers do. Most of those obligations cannot be checked by a
compiler: returning an already-wrapped chat client, capturing a scoped
service, rejecting a model the catalog does not list, or building a client
factory that is not safe to run twice all compile cleanly and misbehave
only later. Deriving this class asserts the ones observable from outside.

No scenario here performs a model call. Every test uses
[IModelProvider.CreateChatClient](/api/tracon.imodelprovider/#Tracon_IModelProvider_CreateChatClient_Tracon_ModelBinding_), [IModelProvider.Name](/api/tracon.imodelprovider/#Tracon_IModelProvider_Name)
and [IModelProvider.Models](/api/tracon.imodelprovider/#Tracon_IModelProvider_Models) only, so the suite runs with no
network access and no API key. An implementation whose
`CreateChatClient` reaches the network to construct a client is itself
breaking the contract: construction is on the compile path.

Two obligations are **not** checked here because they are not
observable from outside a single provider instance: that the provider holds
no captured scoped service, and that the registry rejects two providers
sharing a name. Both are stated in [IModelProvider](/api/tracon.imodelprovider/)'s own
documentation.

This class carries the core every provider owes. Two derived classes add
more. [ModelProviderCredentialContract](/api/tracon.testing.contracts.providers.modelprovidercredentialcontract/) covers honoring a
per-tenant credential (BYOK); it is opt-in, so deriving it is the
statement of intent and no scenario has to silently pass. A provider that
does not offer BYOK records that in its `ContractCoverage`
exemptions. [ModelProviderSettingsContract](/api/tracon.testing.contracts.providers.modelprovidersettingscontract/) covers
[ModelBinding.ProviderSettings](/api/tracon.modelbinding/#Tracon_ModelBinding_ProviderSettings) and is owed by every provider,
including one that supports no key and so rejects them all.

## Constructors

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract__ctor"></a> ModelProviderContract\(\)

```csharp
protected ModelProviderContract()
```

## Properties

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Provider"></a> Provider

The provider under test.

```csharp
protected IModelProvider Provider { get; }
```

#### Property Value

 [IModelProvider](/api/tracon.imodelprovider/)

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_UnknownModelName"></a> UnknownModelName

A model name the provider's catalog does not list. The contract
requires the provider to accept it: the catalog is metadata, not an
allow list.

```csharp
protected virtual string UnknownModelName { get; }
```

#### Property Value

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

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_A_client_that_is_never_disposed_does_not_stop_the_provider"></a> A\_client\_that\_is\_never\_disposed\_does\_not\_stop\_the\_provider\(\)

```csharp
[Fact]
public void A_client_that_is_never_disposed_does_not_stop_the_provider()
```

#### Remarks

Tracon never disposes the client this method returns: the compiled
agent holds it and 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. A provider must therefore
stay usable after handing out a client nobody disposes.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_A_model_the_catalog_does_not_list_is_not_rejected"></a> A\_model\_the\_catalog\_does\_not\_list\_is\_not\_rejected\(\)

```csharp
[Fact]
public void A_model_the_catalog_does_not_list_is_not_rejected()
```

#### Remarks

The catalog drives the console's picker, capability hints and cost
reporting; it is not an allow list. Rejecting an unlisted model would
mean a newly published model needs a new release before it can be used.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Binding_System_String_System_String_"></a> Binding\(string?, string?\)

A binding aimed at this provider.

```csharp
protected ModelBinding Binding(string? model = null, string? providerName = null)
```

#### Parameters

`model` [string](https://learn.microsoft.com/dotnet/api/system.string)?

The model name. Defaults to the first catalog entry, or
[ModelProviderContract.UnknownModelName](/api/tracon.testing.contracts.providers.modelprovidercontract/#Tracon_Testing_Contracts_Providers_ModelProviderContract_UnknownModelName) when the catalog is empty — an empty
catalog is a supported state.

`providerName` [string](https://learn.microsoft.com/dotnet/api/system.string)?

The provider name to put in the binding. Defaults to
[IModelProvider.Name](/api/tracon.imodelprovider/#Tracon_IModelProvider_Name).

#### Returns

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

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Catalog_model_names_are_unique_and_not_empty"></a> Catalog\_model\_names\_are\_unique\_and\_not\_empty\(\)

```csharp
[Fact]
public void Catalog_model_names_are_unique_and_not_empty()
```

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Concurrent_create_chat_client_calls_all_return_a_client"></a> Concurrent\_create\_chat\_client\_calls\_all\_return\_a\_client\(\)

```csharp
[Fact]
public Task Concurrent_create_chat_client_calls_all_return_a_client()
```

#### Returns

 [Task](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task)

#### Remarks

Providers are registered as singletons and the registry keeps one
instance, so this method is called concurrently from the compile path.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_CreateProviderAsync"></a> CreateProviderAsync\(\)

Produces the provider to test.

```csharp
protected abstract ValueTask<IModelProvider> CreateProviderAsync()
```

#### Returns

 [ValueTask](https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask\-1)<[IModelProvider](/api/tracon.imodelprovider/)\>

A provider ready for use.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Create_chat_client_accepts_a_binding_whose_provider_name_differs_in_case"></a> Create\_chat\_client\_accepts\_a\_binding\_whose\_provider\_name\_differs\_in\_case\(\)

```csharp
[Fact]
public void Create_chat_client_accepts_a_binding_whose_provider_name_differs_in_case()
```

#### Remarks

[ModelBinding.Provider](/api/tracon.modelbinding/#Tracon_ModelBinding_Provider) is matched against
[IModelProvider.Name](/api/tracon.imodelprovider/#Tracon_IModelProvider_Name) case-insensitively, so a binding may
legitimately carry a different casing than the provider registered. A
provider that compares the two with an ordinal check and throws breaks
on a binding the registry considers valid.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Create_chat_client_returns_a_client_for_the_setup_time_credential"></a> Create\_chat\_client\_returns\_a\_client\_for\_the\_setup\_time\_credential\(\)

```csharp
[Fact]
public void Create_chat_client_returns_a_client_for_the_setup_time_credential()
```

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Create_chat_client_returns_a_raw_client_that_builds_no_tool_call_loop"></a> Create\_chat\_client\_returns\_a\_raw\_client\_that\_builds\_no\_tool\_call\_loop\(\)

```csharp
[Fact]
public void Create_chat_client_returns_a_raw_client_that_builds_no_tool_call_loop()
```

#### Remarks

The single most consequential rule in [IModelProvider](/api/tracon.imodelprovider/):
the common pipeline belongs to `ModelProviderRegistry`. A provider
that builds its own `UseFunctionInvocation` loop puts the turn
carrying a tool result back into the model *beneath* the content
guard, which is exactly the path prompt injection takes — and nothing
in the reply text or the tool-call count reveals it.
`GetService` is Microsoft.Extensions.AI's own discovery protocol
and surfaces the loop through any number of nested rings, so this holds
however the provider decorates its client.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_DisposeAsync"></a> DisposeAsync\(\)

```csharp
public ValueTask DisposeAsync()
```

#### Returns

 [ValueTask](https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask)

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_InitializeAsync"></a> InitializeAsync\(\)

Called immediately after the class has been created, before it is used.

```csharp
public ValueTask InitializeAsync()
```

#### Returns

 [ValueTask](https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask)

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_OnDisposeAsync"></a> OnDisposeAsync\(\)

Hook for a derived class to release its own resources.

```csharp
protected virtual ValueTask OnDisposeAsync()
```

#### Returns

 [ValueTask](https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask)

A completed task.

### <a id="Tracon_Testing_Contracts_Providers_ModelProviderContract_Provider_name_is_not_empty"></a> Provider\_name\_is\_not\_empty\(\)

```csharp
[Fact]
public void Provider_name_is_not_empty()
```
