vDeskConnect generates lesson notes, schemes of work, and exams from curriculum data. The generation runs through an 871-line Laravel service — and the most important design decision in it is that no single AI provider is load-bearing.
Three providers, one boundary
The service talks to Gemini, OpenAI-compatible endpoints, and Qwen. Everything the rest of the app knows is one method:
public function generateLessonNote(array $schemeData, array $gradeInfo, array $subjectInfo, int $audienceSize = 30): array
Curriculum in, structured note out — plus a
_used_fallback flag so callers always know whether
they got the primary brain or the spare. Provider choice is
configuration, not code.
Configuration, not code
Provider, key, endpoint, and model all resolve from the environment with sane defaults:
$this->apiKey = env('AI_API_KEY');
$this->baseUrl = env('AI_BASE_URL', 'https://generativelanguage.googleapis.com/v1beta');
$this->model = env('AI_MODEL', 'gemini-2.0-flash');
$this->provider = env('AI_PROVIDER', 'gemini');
Switching providers in production is a config change and a deploy of nothing. When a provider has a bad morning, operations flips a variable instead of paging a developer.
Exams need structure, not prose
Lecture notes tolerate creativity; exams do not. Generation for assessments is constrained to the exam engine's shape — multiple choice, theory, file uploads — so AI output always lands in a gradable structure. The model proposes; the schema disposes.
Flag the fallback
Every generation returns whether a fallback was used. That flag feeds monitoring, lets the UI show a gentle "generated with backup" state, and — most importantly — makes provider incidents visible instead of silently degrading quality for weeks.
Takeaway
AI features fail at 3am when a provider does. Isolate generation behind one service, drive provider choice from the environment, constrain outputs to your domain's shape, and always report which brain answered. The feature stays up; only the badge changes.