I have found “dogfooding” is one of the fastest ways to expose any rough edges. So, I got one my Mistral.AI Client applications to generate the code for a Mistral.AI Chat Completion client.

The output looked reasonable, so I copied and paste the code into an empty application.
Enter chat message: console application which calls he MistralAI Chat completion endpoint, api & model configuration loaded from appsetings, usersecrets for API-Key, should use typed DTOs, buffered API calls only (no streaming), httpClientFactory with resilence for http call. The application should wait for user to enter a prompt, if blank prompt program exits. If valid prompt request object generated, then text response displayed
# MistralAI Console Application with Resilient HTTP Calls
Here's a complete console application that meets your requirements. It uses:
- Configuration from appsettings.json
- User secrets for API key
- Typed DTOs
- HttpClientFactory with resilience
- Buffered API calls (no streaming)
- Interactive prompt input
## Project Structure
1. First, create a new console application:
```bash
dotnet new console -n MistralAIConsole
cd MistralAIConsole
```
2. Add required NuGet packages:
```bash
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Http
dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package System.Text.Json
```
## Implementation
### 1. Configuration Classes
Create `Models/Configuration.cs`:
```csharp
namespace MistralAIConsole.Models;
public class MistralAIConfiguration
{
public string ApiUrl { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
public int MaxTokens { get; set; } = 1024;
public double Temperature { get; set; } = 0.7;
}
public class AppConfiguration
{
public MistralAIConfiguration MistralAI { get; set; } = new();
}
```
### 2. DTO Classes
Create `Models/RequestResponse.cs`:
```csharp
using System.Text.Json.Serialization;
namespace MistralAIConsole.Models;
public class ChatCompletionRequest
{
[JsonPropertyName("model")]
public string Model { get; set; } = string.Empty;
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = new();
[JsonPropertyName("max_tokens")]
public int MaxTokens { get; set; } = 1024;
[JsonPropertyName("temperature")]
public double Temperature { get; set; } = 0.7;
}
public class ChatMessage
{
[JsonPropertyName("role")]
public string Role { get; set; } = "user";
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
public class ChatCompletionResponse
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("object")]
public string Object { get; set; } = string.Empty;
[JsonPropertyName("created")]
public long Created { get; set; }
[JsonPropertyName("model")]
public string Model { get; set; } = string.Empty;
[JsonPropertyName("choices")]
public List<ChatChoice> Choices { get; set; } = new();
[JsonPropertyName("usage")]
public UsageInfo Usage { get; set; } = new();
}
public class ChatChoice
{
[JsonPropertyName("index")]
public int Index { get; set; }
[JsonPropertyName("message")]
public ChatMessage Message { get; set; } = new();
[JsonPropertyName("finish_reason")]
public string FinishReason { get; set; } = string.Empty;
}
public class UsageInfo
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}
```
### 3. Service Interface and Implementation
Create `Services/IChatService.cs`:
```csharp
using MistralAIConsole.Models;
namespace MistralAIConsole.Services;
public interface IChatService
{
Task<ChatCompletionResponse> GetChatCompletionAsync(ChatCompletionRequest request);
}
```
Create `Services/ChatService.cs`:
```csharp
using System.Net.Http.Json;
using System.Text.Json;
using MistralAIConsole.Models;
using Microsoft.Extensions.Options;
namespace MistralAIConsole.Services;
public class ChatService : IChatService
{
private readonly HttpClient _httpClient;
private readonly MistralAIConfiguration _config;
public ChatService(HttpClient httpClient, IOptions<MistralAIConfiguration> config)
{
_httpClient = httpClient;
_config = config.Value;
}
public async Task<ChatCompletionResponse> GetChatCompletionAsync(ChatCompletionRequest request)
{
var response = await _httpClient.PostAsJsonAsync("chat/completions", request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ChatCompletionResponse>(content) ??
throw new InvalidOperationException("Failed to deserialize response");
}
}
```
### 4. Program.cs
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Http;
using MistralAIConsole.Models;
using MistralAIConsole.Services;
using Polly;
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddUserSecrets<Program>()
.Build();
var services = new ServiceCollection();
services.Configure<AppConfiguration>(configuration);
services.Configure<MistralAIConfiguration>(configuration.GetSection("MistralAI"));
services.AddHttpClient<IChatService, ChatService>(client =>
{
var mistralConfig = configuration.GetSection("MistralAI").Get<MistralAIConfiguration>();
client.BaseAddress = new Uri(mistralConfig!.ApiUrl);
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {configuration["MistralAI:ApiKey"]}");
})
.AddStandardResilienceHandler()
.Configure<StandardResilienceOptions>(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
});
var serviceProvider = services.BuildServiceProvider();
Console.WriteLine("MistralAI Console Client");
Console.WriteLine("Enter your prompt (or press Enter to exit):");
while (true)
{
Console.Write("> ");
var prompt = Console.ReadLine();
if (string.IsNullOrWhiteSpace(prompt))
{
Console.WriteLine("Exiting...");
break;
}
try
{
var chatService = serviceProvider.GetRequiredService<IChatService>();
var config = serviceProvider.GetRequiredService<IOptions<MistralAIConfiguration>>().Value;
var request = new ChatCompletionRequest
{
Model = config.Model,
MaxTokens = config.MaxTokens,
Temperature = config.Temperature,
Messages = new List<ChatMessage>
{
new() { Role = "user", Content = prompt }
}
};
Console.WriteLine("Sending request to MistralAI...");
var response = await chatService.GetChatCompletionAsync(request);
Console.WriteLine("\nResponse:");
Console.WriteLine(response.Choices.FirstOrDefault()?.Message?.Content ?? "No response content");
Console.WriteLine($"\nTokens used: {response.Usage.TotalTokens}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Console.WriteLine();
}
await Task.CompletedTask;
```
### 5. Configuration Files
Create `appsettings.json`:
```json
{
"MistralAI": {
"ApiUrl": "https://api.mistral.ai/v1/",
"Model": "mistral-tiny",
"MaxTokens": 1024,
"Temperature": 0.7
}
}
```
Initialize user secrets:
```bash
dotnet user-secrets init
dotnet user-secrets set "MistralAI:ApiKey" "your-api-key-here"
```
## How to Use
1. Replace `your-api-key-here` in the user secrets with your actual MistralAI API key
2. Run the application:
```bash
dotnet run
```
3. Enter your prompt when prompted
4. The application will display the response from MistralAI
5. Press Enter without typing anything to exit
## Features
- Resilient HTTP calls with retry and circuit breaker patterns
- Configuration loaded from appsettings.json and user secrets
- Typed DTOs for request/response
- Buffered API calls (no streaming)
- Interactive prompt input
- Proper error handling
The application will automatically exit when an empty prompt is entered.
Prompt tokens: 87
Completion tokens: 1948
Total tokens: 2035
Enter chat message:
It was looking really promising until I tried to compile the code. The resilience handler configuration code was broken so I tried Github Copilot but the suggested fix was overkill with lots of “magic numbers”. I then manually fixed the issue by deleting some code (less is more).
var services = new ServiceCollection();
services.Configure<AppConfiguration>(configuration);
services.Configure<MistralAIConfiguration>(configuration.GetSection("MistralAI"));
/* Original code with broken resilience handler configuration
services.AddHttpClient<IChatService, ChatService>(client =>
{
var mistralConfig = configuration.GetSection("MistralAI").Get<MistralAIConfiguration>();
client.BaseAddress = new Uri(mistralConfig!.ApiUrl);
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {configuration["MistralAI:ApiKey"]}");
})
.AddStandardResilienceHandler()
.Configure<StandardResilienceOptions>(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
});
*/
// Smallest fix for the resilience handler
services.AddHttpClient<IChatService, ChatService>(client =>
{
var mistralConfig = configuration.GetSection("MistralAI").Get<MistralAIConfiguration>();
client.BaseAddress = new Uri(mistralConfig!.ApiUrl);
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {configuration["MistralAI:ApiKey"]}");
}).AddStandardResilienceHandler();
/* Copilot's fix for the resilience handler, way more complex the required for PoC
services.AddHttpClient<IChatService, ChatService>(client =>
{
var mistralConfig = configuration.GetSection("MistralAI").Get<MistralAIConfiguration>();
client.BaseAddress = new Uri(mistralConfig!.ApiUrl);
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {configuration["MistralAI:ApiKey"]}");
})
.AddStandardResilienceHandler()
.Configure(options =>
{
// Retry
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
// Circuit breaker — SamplingDuration must be >= 2 * AttemptTimeout.Timeout
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30); // was 10 → invalid
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
// Give the retry budget room:
// MaxRetryAttempts * (Retry.Delay + AttemptTimeout.Timeout) <= TotalRequestTimeout.Timeout
// 3 * (1s + 10s) = 33s → must raise TotalRequestTimeout above 33s
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
});
*/
var serviceProvider = services.BuildServiceProvider();
The Anthropic Claude review exported as markdown hence the odd formatting. I couldn’t export Copilot Chat
Code Review: TypedDTOsGeneratedClient
Reviewed files: Program.cs, Configuration.cs, RequestResponse.cs, Services/ChatService.cs, Services/IChatService.cs, TypedDTOsGeneratedClient.csproj, appsettings.json
Bugs / correctness
-
Swallowed error detail on failed HTTP calls (
Services/ChatService.cs:22)response.EnsureSuccessStatusCode()throws before the response body is read. Mistral’s API returns useful error detail (e.g. auth failures, rate limits, invalid model) in the JSON body, but that’s discarded — the user just sees"Response status code does not indicate success: 401 (Unauthorized)."in the console. Read the body first and include it in a thrown exception when the status isn’t successful. -
serviceProvideris never disposed (Program.cs:78)BuildServiceProvider()returns anIDisposable(it owns theHttpClient/IHttpClientFactoryand resilience handlers). Should beusing var serviceProvider = ....
Dead code / cleanup
-
~55 lines of commented-out code (
Program.cs:21-76) Three abandoned attempts at configuringAddStandardResilienceHandler(original, “smallest fix”, “Copilot’s fix”), left in as block comments. This is exactly what git history is for — worth deleting now that the active version works, so the file isn’t carrying a debugging diary. -
AppConfigurationis registered but never consumed (Program.cs:18,Configuration.cs:11-14)services.Configure(configuration)binds the whole root config, but nothing ever injectsIOptions; onlyIOptionsis used. Either use it or drop the registration and the class.
Design inconsistency
ApiKeyisn’t part ofMistralAIConfiguration(Configuration.cs:3-9vsProgram.cs:26,46)ApiUrl,Model,MaxTokens,Temperatureare all strongly-typed, but the API key is read via the raw string indexerconfiguration["MistralAI:ApiKey"]. If deliberate (to avoid the secret riding along on the options object), a one-line comment would save the next reader from wondering; otherwise it’s a gap in an otherwise “Typed DTOs” project.
Minor / style
ChatService.GetChatCompletionAsyncmanually doesReadAsStringAsync+JsonSerializer.Deserialize(ChatService.cs:24-25) whereresponse.Content.ReadFromJsonAsync()would do the same in one call and reuse the DI-configured serializer options.- No
CancellationTokenanywhere in the async chain (IChatService,ChatService,Program.cs’s loop) — fine for a console PoC, but worth noting if this client graduates beyond a demo. mistralConfig!.ApiUrl(Program.cs:45) uses null-forgiving on a config section that could legitimately be missing/misspelled — would NRE with no context if theMistralAIsection is absent fromappsettings.json.
Security notes
Nothing security-sensitive stood out — the API key correctly comes from user secrets, not appsettings.json (confirmed appsettings.json has no ApiKey field), and it’s not logged anywhere.
“Dogfooding" my client forced me to revisit the DTOs (especially the polymorphic ones), refine the resilience strategy, and debug the implementation of the prompt loop.
Overall, for a PoC the code and review results were surprisingly good. I have found “teaming” using Mistral Codestral, Anthropic Claude and Github Copilot to generate and then review each other’s code.























