Skip to main content

Technology summary

AspectChoice
Runtime.NET 8
FrameworkASP.NET Core Web API
LanguageC# with nullable reference types
SerializationNewtonsoft.Json (camelCase, ignore nulls, UTC dates)
AuthJWT Bearer (Supabase-issued, HS256)
API DocsSwashbuckle / Swagger
Database ClientRaw HTTP calls to Supabase PostgREST API
DI ContainerBuilt-in Microsoft.Extensions.DependencyInjection

Controller map

Every controller inherits from ControllerBase (no views). Routes use attribute routing.

Public API controllers

ControllerRoute prefixAuthPurpose
HealthController/health, /api/healthAnonymousHealth checks
ArenaController/api/arenaBearer JWTChat, dual chat, streaming
ModelsController/api/modelsBearer JWTList active AI models
ThreadsController/api/threadsBearer JWTThread CRUD + messages
UsersController/api/usersBearer JWTUser sync
SettingsController/api/settingsAnonymousFeature flags
SpeechController/api/speechBearer JWTText-to-speech

Admin API controllers

All admin controllers are under /api/admin/ and use IAdminSupabaseClient for data access with service role key.
ControllerRoute prefixPurpose
AdminDashboardController/api/admin/dashboardStats, activity, performance
AdminAIModelsController/api/admin/modelsAI model CRUD
AdminUsersController/api/admin/usersUser CRUD
AdminComparisonsController/api/admin/comparisonsComparison CRUD
AdminModelVotesController/api/admin/votesVote CRUD + stats
AdminThreadsController/api/admin/threadsThread CRUD
AdminThreadMessagesController/api/admin/messagesMessage CRUD
ProvidersController/api/admin/providersProvider + API key management

ArenaController — the core

The ArenaController (Controllers/Api/ArenaController.cs) is the heart of DualMind. It handles all chat operations.

Dependencies injected

private readonly IModelSelector _modelSelector;           // Random model selection from DB
private readonly IChatProviderFactory _chatProviderFactory; // Routes to Groq/Bytez
private readonly IMessageLogger _messageLogger;           // Logs chat messages
private readonly IThreadMessagesService _threadMessagesService; // Thread message persistence
private readonly ILeaderboardModelSelector _leaderboardModelSelector; // "Topper" mode selection
private readonly IComparisonLogger _comparisonLogger;     // Logs dual-chat comparisons
private readonly IUserSyncService _userSyncService;       // Ensures user exists in DB

Single chat flow (POST /api/arena/chat)

  1. Validate prompt is not empty
  2. Select model: if request.Model is "auto" or null, call _modelSelector.GetRandomModelAsync(); otherwise use the specified model
  3. Call ExecuteWithFallbackAsync(model, prompt, system, maxTokens, temperature)
  4. Build ChatResponse with output content, model info, usage stats, response time
  5. Log message via _messageLogger.LogMessageAsync()
  6. If request.ThreadId is set, persist to thread via _threadMessagesService.LogSingleAsync()
  7. Return response

Dual chat flow (POST /api/arena/dualchat)

  1. Validate prompt
  2. Determine selection mode:
    • Manual: Both model1 and model2 specified by client
    • Topper: _leaderboardModelSelector.GetTopperAndRandomModelAsync() — top-rated model vs random
    • Random (default): _modelSelector.GetTwoRandomModelsAsync()
  3. Execute both models in parallel via Task.WhenAll(task1, task2)
  4. Build two ChatResponse objects
  5. Log both messages and the comparison
  6. Compute arena metrics (winner by length, winner by tokens, verdict)
  7. Return { agent1, agent2, comparisonId, arena: { comparison, models } }

Fallback logic (ExecuteWithFallbackAsync)

private async Task<(GroqResponse Response, string UsedModel)> ExecuteWithFallbackAsync(...)
{
    // 1. Resolve provider from model metadata (default: groq)
    // 2. Try primary provider with 45-second timeout
    // 3. On failure:
    //    - If provider != groq → fallback to Groq with llama-3.3-70b-versatile
    //    - If groq failed → try alternative Groq model (llama-3.3-70b-versatile)
    // 4. If all fail → throw with combined error message
}

Service layer

All business logic is in Core/Services/. Services are registered as Scoped except ModelSelector which is Singleton.
ServiceInterfaceResponsibility
ModelSelectorIModelSelectorQuery active models from DB, random selection, model info lookup
LeaderboardModelSelectorILeaderboardModelSelectorSelect top-rated model + random opponent
ThreadsServiceIThreadsServiceThread CRUD, visibility management
ThreadMessagesServiceIThreadMessagesServiceLog single/dual messages to threads
ModelStatsServiceIModelStatsServiceVoting statistics, win rates
ComparisonLoggerIComparisonLoggerPersist comparison records
MessageLoggerIMessageLoggerPersist individual chat messages
UserSyncServiceIUserSyncServiceEnsure public.users row exists before FK operations
SystemSettingsServiceISystemSettingsServiceFeature flag queries
ProviderConfigServiceIProviderConfigServiceAPI key rotation, cooldowns, error tracking

Data access layer

DualMind does not use Entity Framework. It makes direct HTTP calls to the Supabase PostgREST API.

ISupabaseService (user-facing)

Used by public controllers. Configured with service role key in HttpClient default headers.
public interface ISupabaseService
{
    Task<List<T>> SelectAsync<T>(string table, string columns, string query);
    // ... other methods
}

IAdminSupabaseClient (admin-facing)

Used by admin controllers. Provides generic CRUD operations:
public interface IAdminSupabaseClient
{
    Task<string> GetAllAsync(string table, string query);
    Task<string> GetByIdAsync(string table, string idColumn, string id);
    Task<HttpResponseMessage> CreateAsync(string table, object data);
    Task<HttpResponseMessage> UpdateAsync(string table, string idColumn, string id, object data);
    Task<HttpResponseMessage> DeleteAsync(string table, string column, string value);
    Task<int> CountFastAsync(string table, string idColumn, string filterQuery = null);
}

Middleware pipeline

Configured in Program.cs, executed in order:
  1. Exception handler — catches unhandled exceptions, returns ProblemDetails JSON
  2. Request logging — logs correlation ID, method, path, duration, status code
  3. CORSAllowAll policy (any origin, method, header)
  4. HTTPS redirection
  5. Authentication — JWT Bearer validation
  6. Authorization[Authorize] attribute enforcement
  7. Controller routingapp.MapControllers()

Error response format

All errors follow a consistent shape:
{
  "object": "ai.error",
  "code": "INVALID_REQUEST",
  "message": "Prompt is required and cannot be empty",
  "timestamp": "2024-01-03T10:00:00Z"
}
Error codes: INVALID_REQUEST, API_ERROR, STREAM_ERROR, THREADS_ERROR, THREAD_CREATE_ERROR, MODELS_ERROR, NOT_FOUND, UNAUTHORIZED, FORBIDDEN.