All projects

Bachelor's Project, University of Southern Denmark

AI Integration Architecture (TrafikGPT)

Software ArchitectureNext.jsVercel AI SDKVercel AI GatewayExaLLM

TrafikGPT is the prototype from my bachelor's project in Software Engineering at the University of Southern Denmark: an AI driving instructor for TeoriOnline.dk that answers questions about the Danish driving theory test. The chatbot itself is the visible part. The real subject of the project was the architecture behind it, and one question in particular: how do you build on AI models without being locked to the vendor that provides them?

The problem

Two competitors, Teoriklar.dk and Teoriundervisning.dk, had already launched AI-based features. For a new entrant like TeoriOnline.dk, an AI assistant was becoming table stakes rather than a differentiator.

At the same time, the AI market moves fast. Models improve every few months, prices change, and APIs get deprecated. Hard-wiring the platform to one provider would mean a rewrite every time a better or cheaper model came along. For a one-person company with limited development hours, that is not an option.

The problem formulation became:

How can a software architecture be designed to support different AI models that act as a virtual driving instructor that provides personalized guidance for driving theory test preparation for TeoriOnline.dk?

Approach

The project follows Attribute-Driven Design (ADD), where architecture is driven by quality attributes rather than features. The work moved from analysis (system description, stakeholders, user stories, use cases) to design (quality attribute scenarios, architectural tactics, views), then implementation and evaluation. Quality attributes are defined as in Software Architecture in Practice by Bass et al.

The analysis produced a clear prioritisation:

Quality attributePriorityWhy
ModifiabilityHighAdd new AI models without architectural redesign
IntegrabilityHighIntegrate many providers through one unified interface
AvailabilityHighKeep the service running during deploys and failures
PerformanceLowMatters for UX, but less than flexibility in version one

These were turned into measurable scenarios. A developer must be able to add a new AI provider in under one hour, replace the active provider in under one hour, and do both without touching application logic. A new version must also deploy without interrupting ongoing conversations.

Constraints

  • Built with Next.js, the same framework as TeoriOnline.dk
  • AI integration through the Vercel AI SDK
  • All providers accessed through a unified interface
  • Application logic must not depend on provider-specific APIs
  • Replacing a model must not require code changes
  • The system must be able to retrieve up-to-date information, since traffic regulations change

Architecture

Every request flows the same way: the chat UI calls a Next.js API route, the route asks a configuration module which model is active, and the request is sent through an AI Gateway that hides everything provider-specific. The UI and the route never know whether they are talking to Anthropic, OpenAI or Google.

The design is realised through a small set of architectural tactics:

TacticHow it shows up
Use an IntermediaryThe API route is the only thing that talks to AI
EncapsulateProvider details live behind the AI Gateway
Configuration-Time BindingThe active provider is chosen in .env, not in code
PolymorphismEvery provider is called through the same interface
OrchestrateThe route coordinates model, system prompt and web search
Exception HandlingErrors are caught, logged and returned in a controlled way

Implementation

The prototype is built with Next.js 16, the Vercel AI SDK and its createGateway function, @ai-sdk/react for the chat UI, the Exa AI SDK for web search, and Tailwind CSS for a layout that works on both mobile and desktop.

One mapping decides the model

The configuration module maps providers to model strings. Selecting a provider is a lookup, and an invalid value falls back to a default instead of crashing:

export const AI_MODELS = {
  anthropic: 'anthropic/claude-sonnet-4.5',
  openai: 'openai/gpt-4o',
  google: 'google/gemini-2.5-flash-lite',
  perplexity: 'perplexity/sonar'
} as const

export function getProvider(): AIProvider {
  const envProvider = process.env.AI_PROVIDER || 'anthropic'
  if (envProvider in AI_MODELS) {
    return envProvider as AIProvider
  }
  console.warn(`[AI] Invalid provider, falling back to "anthropic"`)
  return 'anthropic'
}

One route talks to every provider

The API route never imports a provider SDK. It asks for the current model string and hands it to the gateway, together with the system prompt and the web search tool:

const gateway = createGateway({
  apiKey: process.env.AI_GATEWAY_API_KEY
})

export async function POST(req: Request) {
  try {
    const { messages } = await req.json()
    const modelString = getModelString()

    const result = streamText({
      model: gateway(modelString),
      system: SYSTEM_PROMPT,
      messages: convertToModelMessages(messages),
      tools: { webSearch: webSearch({ ... }) }
    })

    return result.toUIMessageStreamResponse()
  } catch (error) {
    console.error('[AI] Error:', error)
    return new Response('Internal Server Error', { status: 500 })
  }
}

Answers stream to the browser as they are generated, so students see the response build up instead of waiting for the full text.

Swapping provider is a one-line change

# .env
AI_PROVIDER=anthropic   # before
AI_PROVIDER=openai      # after

Change the variable, redeploy, and the platform runs on a different model. The UI and the API route are untouched. Adding a brand new provider means one extra line in AI_MODELS.

Grounded in official sources

A language model's training data goes stale, and driving regulations do not stand still. The system prompt restricts the assistant to Danish driving theory, and an Exa web search tool lets it look things up on official and relevant sources: faerdselsstyrelsen.dk, borger.dk, retsinformation.dk and teorionline.dk.

Evaluation

ScenarioResult
Add a new AI provider in 1 hourAchieved. One line in AI_MODELS, estimated well under 30 min
Replace the active providerAchieved. Change AI_PROVIDER and redeploy, under 30 min
Deploy without user disruptionPartially. Zero-downtime deploys depend on the hosting platform

Modifiability and integrability were fully achieved. Availability was only partially achieved: errors are handled and an invalid provider falls back to a default, but there is no automatic failover to another provider if the active one goes down at runtime. Performance was deliberately deprioritised and has not been formally benchmarked.

Limitations and next steps

  • Questions and answers are not logged yet, only provider selection and errors
  • Chat history is not persisted, so previous conversations cannot be revisited
  • Switching provider still requires a redeploy rather than a runtime setting
  • Automatic runtime failover between providers is the obvious next step for availability

Takeaway

The most valuable part of the project was not the chatbot but the boundary around it. By treating the AI provider as a replaceable detail behind a single interface, TeoriOnline.dk can follow the AI market instead of being tied to it: switch to a cheaper model, try a newer one, or move away from a provider whose prices or terms change, all through configuration.

Try the prototype at trafik-gpt.vercel.app.

All projects