.NET API
ModelProviderContract
Tracon.Testing.Contracts.Xunit.dllBehavior tests for the IModelProvider contract.
public abstract class ModelProviderContract : IAsyncLifetime, IAsyncDisposableInheritance
Section titled “Inheritance”object ← ModelProviderContract
Derived
Section titled “Derived”ModelProviderCredentialContract, ModelProviderSettingsContract
Implements
Section titled “Implements”IAsyncLifetime, IAsyncDisposable
Inherited Members
Section titled “Inherited Members”object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()
Remarks
Section titled “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, IModelProvider.Name
and 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’s own documentation.
This class carries only what every provider owes. Behavior that is
a provider’s choice lives in its own opt-in class, so that deriving it is
the statement of intent and no scenario has to silently pass:
ModelProviderCredentialContract for honoring a per-tenant
credential (BYOK), and ModelProviderSettingsContract for
validating ModelBinding.ProviderSettings. A provider that
offers neither derives neither, and records that in its
ContractCoverage exemptions.
Constructors
Section titled “Constructors”ModelProviderContract()
Section titled “ ModelProviderContract()”protected ModelProviderContract()Properties
Section titled “Properties”Provider
Section titled “ Provider”The provider under test.
protected IModelProvider Provider { get; }Property Value
Section titled “Property Value”UnknownModelName
Section titled “ 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.
protected virtual string UnknownModelName { get; }Property Value
Section titled “Property Value”Methods
Section titled “Methods”A_client_that_is_never_disposed_does_not_stop_the_provider()
Section titled “ A_client_that_is_never_disposed_does_not_stop_the_provider()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 190)]public void A_client_that_is_never_disposed_does_not_stop_the_provider()Remarks
Section titled “Remarks”Tracon never disposes the client this method returns: the compiled agent holds it and implements neither IDisposable nor 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_model_the_catalog_does_not_list_is_not_rejected()
Section titled “ A_model_the_catalog_does_not_list_is_not_rejected()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 159)]public void A_model_the_catalog_does_not_list_is_not_rejected()Remarks
Section titled “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.
Binding(string?, string?)
Section titled “ Binding(string?, string?)”A binding aimed at this provider.
protected ModelBinding Binding(string? model = null, string? providerName = null)Parameters
Section titled “Parameters”model string?
The model name. Defaults to the first catalog entry, or ModelProviderContract.UnknownModelName when the catalog is empty — an empty catalog is a supported state.
providerName string?
The provider name to put in the binding. Defaults to IModelProvider.Name.
Returns
Section titled “Returns”The binding.
Catalog_model_names_are_unique_and_not_empty()
Section titled “ Catalog_model_names_are_unique_and_not_empty()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 98)]public void Catalog_model_names_are_unique_and_not_empty()Concurrent_create_chat_client_calls_all_return_a_client()
Section titled “ Concurrent_create_chat_client_calls_all_return_a_client()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 167)]public Task Concurrent_create_chat_client_calls_all_return_a_client()Returns
Section titled “Returns”Remarks
Section titled “Remarks”Providers are registered as singletons and the registry keeps one instance, so this method is called concurrently from the compile path.
CreateProviderAsync()
Section titled “ CreateProviderAsync()”Produces the provider to test.
protected abstract ValueTask<IModelProvider> CreateProviderAsync()Returns
Section titled “Returns”A provider ready for use.
Create_chat_client_accepts_a_binding_whose_provider_name_differs_in_case()
Section titled “ Create_chat_client_accepts_a_binding_whose_provider_name_differs_in_case()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 143)]public void Create_chat_client_accepts_a_binding_whose_provider_name_differs_in_case()Remarks
Section titled “Remarks”ModelBinding.Provider is matched against 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.
Create_chat_client_returns_a_client_for_the_setup_time_credential()
Section titled “ Create_chat_client_returns_a_client_for_the_setup_time_credential()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 107)]public void Create_chat_client_returns_a_client_for_the_setup_time_credential()Create_chat_client_returns_a_raw_client_that_builds_no_tool_call_loop()
Section titled “ Create_chat_client_returns_a_raw_client_that_builds_no_tool_call_loop()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 122)]public void Create_chat_client_returns_a_raw_client_that_builds_no_tool_call_loop()Remarks
Section titled “Remarks”The single most consequential rule in 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.
DisposeAsync()
Section titled “ DisposeAsync()”public ValueTask DisposeAsync()Returns
Section titled “Returns”InitializeAsync()
Section titled “ InitializeAsync()”Called immediately after the class has been created, before it is used.
public ValueTask InitializeAsync()Returns
Section titled “Returns”OnDisposeAsync()
Section titled “ OnDisposeAsync()”Hook for a derived class to release its own resources.
protected virtual ValueTask OnDisposeAsync()Returns
Section titled “Returns”A completed task.
Provider_name_is_not_empty()
Section titled “ Provider_name_is_not_empty()”[Fact("/Users/farukatasoy/Desktop/projects/Tracon/src/Tracon.Testing.Contracts.Xunit/Contracts/Providers/ModelProviderContract.cs", 94)]public void Provider_name_is_not_empty()