Multi-Model Code Review Pipeline

Route code diffs through three AI agents across Google, Anthropic, and OpenAI.

Introduction

30 Second Summary

When companies build AI-powered tools, they rarely rely on a single model for everything. Different tasks have different strengths, and the smartest systems route each job to the best-fit model.

In this project, you will build a full-stack code review pipeline that sends code through three specialized AI agents, each powered by a different provider. A React dashboard tracks exactly how many tokens each agent consumed and what it cost.

What You'll Build

A FastAPI backend orchestrates three agents (Google Gemini for classification, Anthropic Claude for code review, OpenAI GPT for summarization) while a React frontend visualizes the pipeline execution with per-agent metrics and cost comparisons.

By the end of this project, you'll have:

  • A Python backend with three specialized agent classes, each calling a different LLM provider matched to its task.
  • A FastAPI server that chains the agents with structured context handoffs, passing compact JSON between steps instead of raw conversation history.
  • A React dashboard that displays per-agent token usage, cost, and latency alongside a live comparison against single-model alternatives.
  • Secret Mission: A dynamic routing mode that adjusts which model handles the review based on the code's complexity score.

Are there any prerequisites?

This project requires API keys for three providers: Anthropic, OpenAI, and Google Gemini. You'll set them up as environment variables in Step 1.

You'll also need Python 3.9+ and Node.js installed on your machine. Familiarity with Python and React basics is helpful.

Before We Start

Before We Start...

Set Up the Project

Before you can build the multi-model pipeline, you need a project structure that keeps the Python backend and the React frontend cleanly separated.

In this step, you will create the project folders, install all the backend dependencies, scaffold a React frontend with Vite, and configure your API keys so each SDK can find them automatically.

In this step, get ready to:
  • Create the project directory with a Python virtual environment and install backend packages.
  • Scaffold a React app with Vite and remove the default boilerplate.
  • Export your three API keys as environment variables.
Create the project directory and install backend dependencies

The project needs two top-level folders: backend/ for the Python API server and frontend/ for the React dashboard.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and the backend directory, then move into the backend by running these commands:

macOS

mkdir -p code-review-pipeline/backend
cd code-review-pipeline/backend

Windows

mkdir code-review-pipeline\backend
cd code-review-pipeline\backend
  • Confirm you are inside the backend folder by running:
pwd

You should see a path ending in code-review-pipeline/backend.

A Python virtual environment keeps the packages for this project isolated from any other Python projects on your machine.

  • Create and activate a virtual environment by running these commands:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\Activate

You should see (venv) appear at the beginning of your terminal prompt.

Don't see (venv) in your prompt?

On macOS, if you see command not found: python3, make sure Python 3.9+ is installed by running python3 --version.

On Windows, try python instead of python3. If neither works, reinstall Python from python.org and check the box to add Python to your PATH during installation.

Need more help? Help me troubleshoot my Python virtual environment

The backend needs five Python packages. Three are SDK clients for the AI providers, and two power the API server.

  • Install all five backend dependencies by running:
pip install anthropic openai google-genai fastapi uvicorn

What are these packages?

  • anthropic is the official Python SDK for Anthropic's Claude models.
  • openai is the official Python SDK for OpenAI's GPT models.
  • google-genai is the official Python SDK for Google's Gemini models.
  • fastapi and uvicorn together run the API server that connects the backend agents to the React frontend.

You should see Successfully installed followed by a list of package names in your terminal output.

Seeing a pip install error?

Make sure your virtual environment is active. You should see (venv) in your prompt. If not, re-run the activation command from the previous step.

If you see a permissions error, do not use sudo pip install. Activate the virtual environment first, then try the install again.

Still stuck? Help me fix my pip install error

Scaffold the React frontend with Vite

Vite is a modern build tool that creates a React project with hot reload out of the box.

  • Navigate back to the project root by running:
cd ..
  • Scaffold a new React app inside a frontend folder by running:
npm create vite@latest frontend -- --template react

What does this command do?

npm create vite@latest downloads the latest Vite scaffolding tool. The frontend argument names the folder to create.

The --template react flag tells Vite to use the React template, which skips any interactive prompts.

You should see a message confirming that the project was scaffolded in the frontend directory.

  • Move into the frontend folder and install its dependencies by running:
cd frontend
npm install

You should see added followed by a package count in the output, confirming that all Node dependencies were installed.

npm command not found?

This means Node.js is not on your PATH. Confirm it is installed by running node --version. If nothing appears, install Node.js from nodejs.org before continuing.

Need help? Help me install Node.js on my machine

The default Vite scaffold comes with demo logos, a counter component, and sample styles that you do not need. Time to strip them out.

  • Delete the default assets folder by running:

macOS

rm -rf src/assets

Windows

Remove-Item -Recurse -Force src\assets
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, then select the code-review-pipeline folder and click Open.

You should see both the backend and frontend folders listed in the Explorer sidebar on the left.

  • In the Explorer sidebar, expand frontend then src, and click on App.jsx to open it.
  • Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows), then replace it with the following code:
function App() {
  return <h1>Pipeline Dashboard</h1>
}

export default App

What does this code do?

This is the smallest valid React component. It renders a single heading so you can confirm the app works before you build the real dashboard in a later step.

  • Save App.jsx with Cmd+S (macOS) or Ctrl+S (Windows).
  • Open App.css in the same src folder.
  • Select all content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it so the file is completely empty.
  • Save App.css.
  • Open index.css in the same src folder, select all, delete everything, and save.

Now confirm the cleaned-up app still runs.

  • Back in your terminal, make sure you are inside the frontend folder and start the Vite dev server by running:
npm run dev
  • Open your browser and navigate to http://localhost:5173.

You should see a plain white page with the heading "Pipeline Dashboard" in black text.

  • Press Ctrl+C in your terminal to stop the dev server for now.

Seeing a blank page or an error?

If the page is blank, open the browser console with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows). A common error here is Failed to resolve import, which means App.jsx is still trying to import a file from the deleted assets folder.

Double check that your App.jsx only contains the minimal placeholder code from above and nothing from the original Vite template.

Still stuck? Help me fix my Vite React app

Export your API keys as environment variables

Each of the three AI SDKs auto-detects its API key from a specific environment variable. Setting these now means your Python code can create clients without hard-coding any secrets.

Which variable does each SDK look for?

  • The anthropic SDK reads from ANTHROPIC_API_KEY.
  • The openai SDK reads from OPENAI_API_KEY.
  • The google-genai SDK reads from GEMINI_API_KEY.
  • In your terminal, export all three API keys by running the commands below. Replace each placeholder with your real key.

macOS

export ANTHROPIC_API_KEY=your-anthropic-api-key-here
export OPENAI_API_KEY=your-openai-api-key-here
export GEMINI_API_KEY=your-gemini-api-key-here

Windows

$env:ANTHROPIC_API_KEY = "your-anthropic-api-key-here"
$env:OPENAI_API_KEY = "your-openai-api-key-here"
$env:GEMINI_API_KEY = "your-gemini-api-key-here"

Keep this terminal open

Environment variables exported this way only last for the current terminal session. If you close this terminal, you will need to export them again before running the backend.

  • Verify that one of the keys is set correctly by running:

macOS

echo $ANTHROPIC_API_KEY

Windows

echo $env:ANTHROPIC_API_KEY

You should see your Anthropic API key printed in the terminal. If it shows an empty line or the literal placeholder text, re-run the export command with your real key.

Seeing an empty result?

On macOS, make sure there are no spaces around the = sign in the export command. export ANTHROPIC_API_KEY=sk-... is correct. export ANTHROPIC_API_KEY = sk-... will not work.

Also confirm you are running the echo command in the same terminal session where you exported the keys.

Need help? Help me set up my API keys as environment variables

Your project skeleton is ready. The backend has its virtual environment and packages, the frontend has a clean React scaffold, and all three API keys are loaded. Next up, you will build the three specialized AI agents that power the review pipeline.

Build the Three Specialized Agents

Your environment is set up, your SDKs are installed, and all three API keys are ready to go. Now it's time to build the core of the pipeline: three AI agents, each backed by a different provider.

Each agent is designed for one specific job. The Router classifies what kind of code change it's looking at. The Reviewer does a deep line-by-line analysis. The Summarizer produces a clean final report. Matching the right model to the right task is how production AI systems keep costs low without sacrificing quality.

In this step, get ready to:
  • Create the RouterAgent using Google Gemini for fast classification.
  • Create the ReviewerAgent using Anthropic Claude for deep code analysis.
  • Create the SummarizerAgent using OpenAI GPT for final report generation.
Create the RouterAgent with Google Gemini

The Router is the first agent in the pipeline. Its only job is to classify what kind of code change a diff represents. Since classification is a lightweight task, it doesn't need a powerful (or expensive) model.

Before writing the Router itself, you'll set up the shared utilities that all three agents rely on: a pricing lookup table, a cost calculation helper, and a result class that standardizes each agent's output.

  • Create a new file called agents.py inside your backend/ folder.
  • Add the imports and shared utilities by copying the following code into agents.py:
import json
import time
import anthropic
from openai import OpenAI
from google import genai


# Per-million-token pricing for each model (USD)
PRICING = {
    "gemini-2.5-flash-lite": {"input": 0.10, "output": 0.40},
    "claude-sonnet-4-6": {"input": 3.00, "output": 15.00},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60},
}


def calculate_cost(model, input_tokens, output_tokens):
    # Look up the model's pricing and compute cost in dollars
    pricing = PRICING[model]
    input_cost = input_tokens * pricing["input"] / 1_000_000
    output_cost = output_tokens * pricing["output"] / 1_000_000
    return input_cost + output_cost


class AgentResult:
    """Stores one agent's output alongside its token usage, cost, and latency."""
    def __init__(self, agent_name, model, provider, output, input_tokens, output_tokens, cost, latency):
        self.agent_name = agent_name
        self.model = model
        self.provider = provider
        self.output = output
        self.input_tokens = input_tokens
        self.output_tokens = output_tokens
        self.cost = cost
        self.latency = latency

    def to_dict(self):
        # Serialize for the API response sent to the React frontend
        return {
            "agent": self.agent_name,
            "model": self.model,
            "provider": self.provider,
            "input_tokens": self.input_tokens,
            "output_tokens": self.output_tokens,
            "cost": round(self.cost, 6),
            "latency": round(self.latency, 2),
        }

What does this code do?

  • PRICING is a lookup table that maps each model name to its per-million-token cost. This is how the dashboard will calculate real dollar amounts.
  • calculate_cost() takes a model name and token counts, then returns the cost in dollars. Every agent calls this after getting a response.
  • AgentResult is a standard wrapper for every agent's output. It bundles the AI response text with usage metrics (tokens, cost, latency) so the frontend can display them uniformly.

You won't see any output yet. The next chunk adds the first agent that uses these utilities.

  • Add the RouterAgent class below the AgentResult class in agents.py:
class RouterAgent:
    def __init__(self):
        # Google GenAI SDK reads GEMINI_API_KEY from the environment
        self.client = genai.Client()
        self.model = "gemini-2.5-flash-lite"

    def classify(self, code_diff):
        start = time.time()
        # Ask Gemini to classify the diff into structured categories
        prompt = (
            "Analyze this code diff and classify it. Return a JSON object with:\n"
            '- "change_type": one of "bug_fix", "feature", "refactor", "style", "docs", "test"\n'
            '- "complexity": one of "low", "medium", "high"\n'
            '- "languages": list of programming languages detected\n'
            '- "focus_areas": list of what the reviewer should pay attention to\n\n'
            f"Code diff:\n```\n{code_diff}\n```\n\n"
            "Return ONLY valid JSON, no other text."
        )
        response = self.client.models.generate_content(
            model=self.model,
            contents=prompt,
        )
        latency = time.time() - start
        # Extract token usage from Gemini's response metadata
        usage = response.usage_metadata
        input_tokens = usage.prompt_token_count
        output_tokens = usage.candidates_token_count
        cost = calculate_cost(self.model, input_tokens, output_tokens)
        return AgentResult(
            agent_name="Router",
            model=self.model,
            provider="Google",
            output=response.text,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
            latency=latency,
        )

What does this code do?

  • genai.Client() creates a Google Gemini client. The SDK automatically reads your GEMINI_API_KEY environment variable.
  • The classify() method sends the code diff to Gemini and asks it to return a JSON object with the change type, complexity level, detected languages, and focus areas.
  • After the response arrives, it reads usage_metadata to extract exact token counts, calculates the cost, and wraps everything in an AgentResult.

Why use the cheapest model for routing?

Gemini 2.5 Flash-Lite costs $0.10 per million input tokens. That's 30x cheaper than Claude Sonnet. Classification is a simple task that doesn't need deep reasoning. It just needs to label the diff.

This is the core principle behind multi-model routing: don't use a frontier model for a task a cheap model handles just as well.

  • Save agents.py.

You won't see any output yet. The Router is ready, but it needs the full pipeline server to run. Next, you'll add the Reviewer agent that receives the Router's classification.

Create the ReviewerAgent with Anthropic Claude

The Reviewer does the heavy lifting: a line-by-line code review. This task requires strong reasoning, so it uses Claude Sonnet 4.6, one of the strongest coding models available at $3.00 per million input tokens.

Crucially, the Reviewer doesn't get the Router's raw conversation. It receives only the Router's structured JSON classification. This pattern is called context compression.

  • Add the ReviewerAgent class below the RouterAgent class in agents.py:
class ReviewerAgent:
    def __init__(self):
        # Anthropic SDK reads ANTHROPIC_API_KEY from the environment
        self.client = anthropic.Anthropic()
        self.model = "claude-sonnet-4-6"

    def review(self, code_diff, classification):
        start = time.time()
        # Pass the Router's classification as compressed context in the system prompt
        system_prompt = (
            "You are an expert code reviewer. A routing agent has classified this change as:\n"
            f"{json.dumps(classification, indent=2)}\n\n"
            "Focus your review on the areas identified. Be specific and actionable."
        )
        message = self.client.messages.create(
            model=self.model,
            max_tokens=1024,
            system=system_prompt,
            messages=[
                {
                    "role": "user",
                    "content": (
                        f"Review this code diff:\n```\n{code_diff}\n```\n\n"
                        "Provide your review as a JSON object with:\n"
                        '- "issues": list of objects with "severity" (critical/warning/info), '
                        '"line_hint", "description", "suggestion"\n'
                        '- "summary": one paragraph overall assessment\n'
                        '- "score": 1-10 code quality score\n\n'
                        "Return ONLY valid JSON."
                    ),
                }
            ],
        )
        latency = time.time() - start
        # Anthropic reports usage as input_tokens and output_tokens
        input_tokens = message.usage.input_tokens
        output_tokens = message.usage.output_tokens
        cost = calculate_cost(self.model, input_tokens, output_tokens)
        return AgentResult(
            agent_name="Reviewer",
            model=self.model,
            provider="Anthropic",
            output=message.content[0].text,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
            latency=latency,
        )

What does this code do?

  • anthropic.Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable.
  • The review() method takes both the original code diff and the Router's classification. The classification goes into the system prompt so Claude knows what to focus on.
  • Claude returns a structured review with issues (each with a severity level), an overall summary, and a quality score from 1 to 10.

What is context compression?

Notice that the classification parameter is a small JSON object (a few hundred bytes). The Reviewer never sees the Router's full prompt or response. It only receives the structured output.

This is context compression. By passing a tight JSON handoff instead of raw conversation history, you reduce input tokens for every downstream agent. Fewer tokens means lower cost and a more focused response.

  • Save agents.py.

Two agents done. The Reviewer will receive the Router's classification as a compressed handoff. Next, you'll add the final agent that ties everything together.

Create the SummarizerAgent with OpenAI GPT

The Summarizer is the last agent in the chain. It takes the classification and the review, then produces a clean final report. Synthesis and formatting don't need deep reasoning, so this is another great spot for a cheap model.

GPT-4o-mini from OpenAI costs just $0.15 per million input tokens. It handles structured output well, which makes it ideal for assembling the final report.

  • Add the SummarizerAgent class below the ReviewerAgent class in agents.py:
class SummarizerAgent:
    def __init__(self):
        # OpenAI SDK reads OPENAI_API_KEY from the environment
        self.client = OpenAI()
        self.model = "gpt-4o-mini"

    def summarize(self, classification, review):
        start = time.time()
        # Bundle both prior agents' outputs into a single compressed context object
        compressed_context = {
            "classification": classification,
            "review": review,
        }
        completion = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {
                    "role": "system",
                    "content": "You are a concise technical writer. Synthesize code review findings into a clean, actionable report.",
                },
                {
                    "role": "user",
                    "content": (
                        "Based on this pipeline data, produce a final review report as JSON:\n\n"
                        f"Pipeline data:\n{json.dumps(compressed_context, indent=2)}\n\n"
                        "Return a JSON object with:\n"
                        '- "title": a short title for this review\n'
                        '- "verdict": "approve", "request_changes", or "needs_discussion"\n'
                        '- "key_findings": list of the most important points (max 5)\n'
                        '- "action_items": list of specific things to fix or improve\n'
                        '- "estimated_effort": "trivial", "small", "medium", or "large"\n\n'
                        "Return ONLY valid JSON."
                    ),
                },
            ],
        )
        latency = time.time() - start
        # OpenAI reports usage as prompt_tokens and completion_tokens
        input_tokens = completion.usage.prompt_tokens
        output_tokens = completion.usage.completion_tokens
        cost = calculate_cost(self.model, input_tokens, output_tokens)
        return AgentResult(
            agent_name="Summarizer",
            model=self.model,
            provider="OpenAI",
            output=completion.choices[0].message.content,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
            latency=latency,
        )

What does this code do?

  • OpenAI() creates a client that reads your OPENAI_API_KEY environment variable.
  • The compressed_context dict bundles the classification and review into a single object. This is the same context compression pattern the Reviewer used. The Summarizer never sees the Router's raw prompt or the Reviewer's raw prompt.
  • The final report includes a verdict (approve, request changes, or needs discussion), key findings, action items, and an effort estimate.

Why use a cheap model for summarization?

GPT-4o-mini costs $0.15 per million input tokens. Summarization and formatting are straightforward tasks. The hard reasoning work is already done by the Reviewer.

The Summarizer just needs to restructure existing findings into a clean report. Spending 20x more on a frontier model for this job would be wasted money.

  • Save agents.py.
  • Confirm the file has no syntax errors by running this command in your terminal (make sure you're in the backend/ folder with your virtual environment activated):
python -c "from agents import RouterAgent, ReviewerAgent, SummarizerAgent; print('All agents loaded successfully')"
  • You should see All agents loaded successfully in the terminal.

Seeing an import error?

  • If you see ModuleNotFoundError, make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • If you see SyntaxError, double check that every parenthesis, bracket, and quote in agents.py is properly closed. Use the full code tab below to compare.
  • Make sure you're running the command from the backend/ folder, not the project root. Run cd backend if needed.

Why can't Python find my agent classes?

✔️ Awesome, I've got everything!

Great. Double check you've saved agents.py and you saw the success message in your terminal.

ⓧ I'd like to double check the full code

Here is the complete agents.py file. Compare it against yours to spot any differences.

import json
import time
import anthropic
from openai import OpenAI
from google import genai


PRICING = {
    "gemini-2.5-flash-lite": {"input": 0.10, "output": 0.40},
    "claude-sonnet-4-6": {"input": 3.00, "output": 15.00},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60},
}


def calculate_cost(model, input_tokens, output_tokens):
    pricing = PRICING[model]
    input_cost = input_tokens * pricing["input"] / 1_000_000
    output_cost = output_tokens * pricing["output"] / 1_000_000
    return input_cost + output_cost


class AgentResult:
    def __init__(self, agent_name, model, provider, output, input_tokens, output_tokens, cost, latency):
        self.agent_name = agent_name
        self.model = model
        self.provider = provider
        self.output = output
        self.input_tokens = input_tokens
        self.output_tokens = output_tokens
        self.cost = cost
        self.latency = latency

    def to_dict(self):
        return {
            "agent": self.agent_name,
            "model": self.model,
            "provider": self.provider,
            "input_tokens": self.input_tokens,
            "output_tokens": self.output_tokens,
            "cost": round(self.cost, 6),
            "latency": round(self.latency, 2),
        }


class RouterAgent:
    def __init__(self):
        self.client = genai.Client()
        self.model = "gemini-2.5-flash-lite"

    def classify(self, code_diff):
        start = time.time()
        prompt = (
            "Analyze this code diff and classify it. Return a JSON object with:\n"
            '- "change_type": one of "bug_fix", "feature", "refactor", "style", "docs", "test"\n'
            '- "complexity": one of "low", "medium", "high"\n'
            '- "languages": list of programming languages detected\n'
            '- "focus_areas": list of what the reviewer should pay attention to\n\n'
            f"Code diff:\n```\n{code_diff}\n```\n\n"
            "Return ONLY valid JSON, no other text."
        )
        response = self.client.models.generate_content(
            model=self.model,
            contents=prompt,
        )
        latency = time.time() - start
        usage = response.usage_metadata
        input_tokens = usage.prompt_token_count
        output_tokens = usage.candidates_token_count
        cost = calculate_cost(self.model, input_tokens, output_tokens)
        return AgentResult(
            agent_name="Router",
            model=self.model,
            provider="Google",
            output=response.text,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
            latency=latency,
        )


class ReviewerAgent:
    def __init__(self):
        self.client = anthropic.Anthropic()
        self.model = "claude-sonnet-4-6"

    def review(self, code_diff, classification):
        start = time.time()
        system_prompt = (
            "You are an expert code reviewer. A routing agent has classified this change as:\n"
            f"{json.dumps(classification, indent=2)}\n\n"
            "Focus your review on the areas identified. Be specific and actionable."
        )
        message = self.client.messages.create(
            model=self.model,
            max_tokens=1024,
            system=system_prompt,
            messages=[
                {
                    "role": "user",
                    "content": (
                        f"Review this code diff:\n```\n{code_diff}\n```\n\n"
                        "Provide your review as a JSON object with:\n"
                        '- "issues": list of objects with "severity" (critical/warning/info), '
                        '"line_hint", "description", "suggestion"\n'
                        '- "summary": one paragraph overall assessment\n'
                        '- "score": 1-10 code quality score\n\n'
                        "Return ONLY valid JSON."
                    ),
                }
            ],
        )
        latency = time.time() - start
        input_tokens = message.usage.input_tokens
        output_tokens = message.usage.output_tokens
        cost = calculate_cost(self.model, input_tokens, output_tokens)
        return AgentResult(
            agent_name="Reviewer",
            model=self.model,
            provider="Anthropic",
            output=message.content[0].text,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
            latency=latency,
        )


class SummarizerAgent:
    def __init__(self):
        self.client = OpenAI()
        self.model = "gpt-4o-mini"

    def summarize(self, classification, review):
        start = time.time()
        compressed_context = {
            "classification": classification,
            "review": review,
        }
        completion = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {
                    "role": "system",
                    "content": "You are a concise technical writer. Synthesize code review findings into a clean, actionable report.",
                },
                {
                    "role": "user",
                    "content": (
                        "Based on this pipeline data, produce a final review report as JSON:\n\n"
                        f"Pipeline data:\n{json.dumps(compressed_context, indent=2)}\n\n"
                        "Return a JSON object with:\n"
                        '- "title": a short title for this review\n'
                        '- "verdict": "approve", "request_changes", or "needs_discussion"\n'
                        '- "key_findings": list of the most important points (max 5)\n'
                        '- "action_items": list of specific things to fix or improve\n'
                        '- "estimated_effort": "trivial", "small", "medium", or "large"\n\n'
                        "Return ONLY valid JSON."
                    ),
                },
            ],
        )
        latency = time.time() - start
        input_tokens = completion.usage.prompt_tokens
        output_tokens = completion.usage.completion_tokens
        cost = calculate_cost(self.model, input_tokens, output_tokens)
        return AgentResult(
            agent_name="Summarizer",
            model=self.model,
            provider="OpenAI",
            output=completion.choices[0].message.content,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cost=cost,
            latency=latency,
        )

All three agents are built and ready. Next up, you'll wire them together into a FastAPI server that chains their outputs into a single pipeline endpoint.

Wire Up the FastAPI Pipeline

Your three agents are built and ready to go. Each one knows how to call its provider, track tokens, and calculate costs.

Now they need an orchestrator. In this step, you will create a FastAPI server that chains the agents into a pipeline, passing compressed context between each handoff instead of raw conversation history.

In this step, get ready to:
  • Create a FastAPI server with CORS middleware for the React frontend.
  • Build a review endpoint that chains all three agents in sequence.
  • Test the full pipeline with a curl request and see real token costs.
Set up the FastAPI server

The backend needs a web server that receives code diffs from the React frontend and returns pipeline results as JSON. FastAPI handles this with automatic request validation and built-in async support.

  • Create a new file called main.py inside your backend/ folder.
  • Add the server setup, CORS middleware, agent instances, and request model by copying this code into main.py:
import json
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from agents import RouterAgent, ReviewerAgent, SummarizerAgent, PRICING


# Create the FastAPI application
app = FastAPI()

# Allow the React dev server (port 5173) to make requests to this backend
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Instantiate all three agents once at startup
router_agent = RouterAgent()
reviewer_agent = ReviewerAgent()
summarizer_agent = SummarizerAgent()


# Define the shape of incoming review requests
class ReviewRequest(BaseModel):
    code_diff: str

What does this code do?

  • The imports bring in your three agent classes and the PRICING dictionary from agents.py.
  • CORS middleware allows the Vite dev server at http://localhost:5173 to reach this backend. Without it, the browser blocks cross-origin requests.
  • The three agents are created once at module level so every incoming request reuses them instead of re-initializing API clients.
  • ReviewRequest uses Pydantic to validate that every request includes a code_diff string. FastAPI rejects malformed requests automatically.
  • Save main.py.

You won't see a change yet. The next chunk adds the endpoint that actually processes review requests.

Build the review endpoint with agent chaining

This is where the pipeline comes together. The endpoint calls each agent in sequence, parsing the previous agent's output into structured JSON before handing it to the next agent.

  • In main.py, add the review endpoint below the ReviewRequest class by copying this code:
@app.post("/api/review")
async def run_review(request: ReviewRequest):
    # Step 1: Router classifies the code diff
    route_result = router_agent.classify(request.code_diff)
    try:
        classification = json.loads(route_result.output)
    except json.JSONDecodeError:
        classification = {
            "change_type": "unknown",
            "complexity": "medium",
            "languages": [],
            "focus_areas": [],
        }

    # Step 2: Reviewer analyzes using the compressed classification
    review_result = reviewer_agent.review(request.code_diff, classification)
    try:
        review = json.loads(review_result.output)
    except json.JSONDecodeError:
        review = {"issues": [], "summary": review_result.output, "score": 0}

    # Step 3: Summarizer synthesizes classification + review into a report
    summary_result = summarizer_agent.summarize(classification, review)
    try:
        summary = json.loads(summary_result.output)
    except json.JSONDecodeError:
        summary = {
            "title": "Review Complete",
            "verdict": "needs_discussion",
            "key_findings": [summary_result.output],
            "action_items": [],
            "estimated_effort": "medium",
        }

What does this code do?

The endpoint chains the three agents in a strict sequence. Each handoff demonstrates context compression.

  • The Router receives the raw code diff and returns a classification JSON (change type, complexity, languages, focus areas).
  • The Reviewer receives only the parsed classification dictionary. It never sees the Router's full prompt or raw response text.
  • The Summarizer receives both the classification and the review as structured JSON objects.
  • Each try/except block handles cases where the LLM returns invalid JSON. The fallback ensures the pipeline continues instead of crashing.

Why compressed handoffs instead of raw conversation?

The Reviewer needs to know what type of change it is reviewing. It does not need the Router's full prompt text or the raw LLM response string.

Passing only the parsed JSON keeps downstream input tokens small and helps each agent focus on its specific task. In production multi-agent systems, this pattern is the difference between a cost-effective pipeline and one that wastes tokens repeating context.

  • Save main.py.

You won't see a change yet. The function still needs a return statement with cost comparison data, which you will add next.

Add cost comparison and test the pipeline

The pipeline now chains all three agents. The final piece is calculating what the same workload would cost if you ran every token through a single model instead of routing.

  • In main.py, add the cost comparison and return statement at the end of the run_review function, directly below the last except json.JSONDecodeError block:
    # Gather totals across all three agents
    agents = [route_result, review_result, summary_result]
    total_input = sum(a.input_tokens for a in agents)
    total_output = sum(a.output_tokens for a in agents)
    total_cost = sum(a.cost for a in agents)

    # Calculate what it would cost to run all tokens through each single model
    single_model_costs = {}
    for model_name, pricing in PRICING.items():
        single_cost = (
            total_input * pricing["input"] / 1_000_000
            + total_output * pricing["output"] / 1_000_000
        )
        single_model_costs[model_name] = round(single_cost, 6)

    # Return per-agent stats, parsed results, totals, and the comparison
    return {
        "pipeline": [a.to_dict() for a in agents],
        "results": {
            "classification": classification,
            "review": review,
            "summary": summary,
        },
        "totals": {
            "input_tokens": total_input,
            "output_tokens": total_output,
            "total_cost": round(total_cost, 6),
        },
        "single_model_comparison": single_model_costs,
    }

What does this code do?

  • total_input, total_output, and total_cost sum up the token counts and dollar cost across all three agents.
  • The single_model_costs loop asks: what if you sent every token through just Gemini Flash-Lite, or just Claude Sonnet, or just GPT-4o-mini? It computes that hypothetical cost for each model using the PRICING dictionary.
  • The response JSON gives the React frontend everything it needs: per-agent metrics, parsed outputs, totals, and the single-model comparison data.

Why is the single-model comparison approximate?

The comparison uses the same total token counts across all models. In reality, different models use different tokenizers, so the exact token count would vary slightly per model. This is a useful simplification for showing relative cost differences.

  • Save main.py.

Time to verify that the full pipeline works end to end.

✔️ Awesome, I've got everything!

Make sure you have saved main.py before moving on.

ⓧ I'd like to double check the full code

import json
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from agents import RouterAgent, ReviewerAgent, SummarizerAgent, PRICING


app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

router_agent = RouterAgent()
reviewer_agent = ReviewerAgent()
summarizer_agent = SummarizerAgent()


class ReviewRequest(BaseModel):
    code_diff: str


@app.post("/api/review")
async def run_review(request: ReviewRequest):
    route_result = router_agent.classify(request.code_diff)
    try:
        classification = json.loads(route_result.output)
    except json.JSONDecodeError:
        classification = {
            "change_type": "unknown",
            "complexity": "medium",
            "languages": [],
            "focus_areas": [],
        }

    review_result = reviewer_agent.review(request.code_diff, classification)
    try:
        review = json.loads(review_result.output)
    except json.JSONDecodeError:
        review = {"issues": [], "summary": review_result.output, "score": 0}

    summary_result = summarizer_agent.summarize(classification, review)
    try:
        summary = json.loads(summary_result.output)
    except json.JSONDecodeError:
        summary = {
            "title": "Review Complete",
            "verdict": "needs_discussion",
            "key_findings": [summary_result.output],
            "action_items": [],
            "estimated_effort": "medium",
        }

    agents = [route_result, review_result, summary_result]
    total_input = sum(a.input_tokens for a in agents)
    total_output = sum(a.output_tokens for a in agents)
    total_cost = sum(a.cost for a in agents)

    single_model_costs = {}
    for model_name, pricing in PRICING.items():
        single_cost = (
            total_input * pricing["input"] / 1_000_000
            + total_output * pricing["output"] / 1_000_000
        )
        single_model_costs[model_name] = round(single_cost, 6)

    return {
        "pipeline": [a.to_dict() for a in agents],
        "results": {
            "classification": classification,
            "review": review,
            "summary": summary,
        },
        "totals": {
            "input_tokens": total_input,
            "output_tokens": total_output,
            "total_cost": round(total_cost, 6),
        },
        "single_model_comparison": single_model_costs,
    }
  • Make sure your terminal is inside the backend/ folder. If not, navigate there by running:
cd ~/Desktop/multi-model-review/backend
  • Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
  • Start the FastAPI server by running:
uvicorn main:app --reload

What does this command do?

Uvicorn is a fast Python web server. main:app tells it to look for the app object in main.py. The --reload flag restarts the server automatically when you save changes.

You should see output like Uvicorn running on http://127.0.0.1:8000 in your terminal.

Seeing a ModuleNotFoundError?

  • Check that your virtual environment is active. You should see (venv) in your terminal prompt. If not, activate it with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
  • Make sure you are running the command from inside the backend/ folder, not the project root. Uvicorn needs to find main.py and agents.py in the current directory.

Help me debug my ModuleNotFoundError when starting the FastAPI server.

  • Open a second terminal window. Keep the server running in the first one.
  • Test the pipeline by sending a code diff with curl:
curl -X POST http://localhost:8000/api/review \
  -H "Content-Type: application/json" \
  -d '{"code_diff": "- x = 1\n+ x = 2"}'

What should I see?

After 10-30 seconds (each agent makes an API call), you should see a JSON response in your terminal. Look for these four top-level keys:

  • pipeline with three objects (one per agent) showing token counts, cost, and latency.
  • results containing the classification, review, and summary output from each agent.
  • totals with the combined input tokens, output tokens, and total cost.
  • single_model_comparison showing what the same workload would cost on each individual model.

Seeing a connection error or 500 response?

  • If you see Connection refused, the server is not running. Check your first terminal for errors and restart it with uvicorn main:app --reload.
  • If you see a 500 error, check the server terminal for a traceback. The most common cause is a missing or invalid API key. Verify that ANTHROPIC_API_KEY, OPENAI_API_KEY, and GEMINI_API_KEY are exported in the terminal where the server is running.
  • If the response comes back but some fields contain fallback values (like "change_type": "unknown"), the agent returned invalid JSON. This is normal for occasional LLM outputs. Try running the curl command again.

Help me debug my FastAPI pipeline response.

Your backend is fully operational. Three agents, three providers, one pipeline. Next up, you will build the React dashboard that puts all of this data on screen.

Build the React Dashboard

Your backend pipeline is live and tested. Three agents can classify, review, and summarize any code diff you throw at them.

Now you need a way to actually use it. In this step, you will build a React dashboard where you paste a code diff, fire it through the pipeline, and see exactly what each agent did, how many tokens it used, and what it cost.

In this step, get ready to:
  • Build a code input section with a sample diff and a button that triggers the pipeline.
  • Display pipeline execution cards showing per-agent metrics and JSON results.
  • Add a cost comparison panel that highlights savings from multi-model routing.
Build the code input section

The dashboard needs a textarea where the user pastes a code diff, a button that sends it to the FastAPI backend, and state management to track loading, errors, and results.

You will pre-load the textarea with a sample Python authentication diff so the learner can test immediately without finding their own code.

  • Make sure your FastAPI server is still running in its terminal. If you stopped it, navigate to your backend folder and start it again by running:
cd ~/Desktop/multi-model-review/backend
source venv/bin/activate
uvicorn main:app --reload
  • Open a second terminal window. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Navigate to your frontend folder by running:
cd ~/Desktop/multi-model-review/frontend
  • Start the Vite dev server by running:
npm run dev

You should see output showing the dev server running at http://localhost:5173. Leave this terminal running.

Port 5173 already in use?

If you see a port conflict, you may have another Vite server running. Close any other terminal windows running npm run dev and try again.

Help me fix a port conflict with Vite

Now it is time to write the React component. The first chunk sets up the imports and a sample code diff that pre-populates the textarea.

  • Open frontend/src/App.jsx in your editor and replace its contents with the following imports and sample diff:
import { useState } from "react";
import "./App.css";

// A sample Python auth diff for quick testing
const SAMPLE_DIFF = `--- a/auth.py
+++ b/auth.py
@@ -15,8 +15,12 @@ def authenticate(username, password):
     user = db.query(User).filter_by(username=username).first()
-    if user and user.password == password:
-        return create_token(user)
+    if user is None:
+        return {"error": "User not found"}, 401
+    if not verify_password(password, user.hashed_password):
+        return {"error": "Invalid credentials"}, 401
+    token = create_token(user)
+    log_login(user.id)
+    return {"token": token}, 200
     return None`;

What does this code do?

The useState import gives you React's state management hook. The App.css import pulls in the stylesheet you will write later in this step.

SAMPLE_DIFF is a realistic Python authentication diff. It shows a plain-text password comparison being replaced with proper hashed password verification. Pre-loading this means you can test the pipeline instantly without hunting for a diff to paste.

  • Below the SAMPLE_DIFF constant, add the full App component with state management, the API call function, and the input section JSX:
function App() {
  // Track the diff text, API results, loading state, and errors
  const [codeDiff, setCodeDiff] = useState(SAMPLE_DIFF);
  const [results, setResults] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // POST the diff to the FastAPI backend and store the response
  const runReview = async () => {
    setLoading(true);
    setError(null);
    setResults(null);
    try {
      const response = await fetch("http://localhost:8000/api/review", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ code_diff: codeDiff }),
      });
      if (!response.ok) throw new Error("Server error: " + response.status);
      const data = await response.json();
      setResults(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Format tiny costs to 6 decimal places for readability
  const formatCost = (cost) => {
    if (cost < 0.01) return "$" + cost.toFixed(6);
    return "$" + cost.toFixed(4);
  };

  return (
    <div className="app">
      <header>
        <h1>Multi-Model Code Review Pipeline</h1>
        <p className="subtitle">
          Three providers. Three agents. One smart pipeline.
        </p>
      </header>

      <div className="input-section">
        <label htmlFor="diff-input">Paste a code diff</label>
        <textarea
          id="diff-input"
          value={codeDiff}
          onChange={(e) => setCodeDiff(e.target.value)}
          rows={15}
          spellCheck={false}
        />
        <button onClick={runReview} disabled={loading || !codeDiff.trim()}>
          {loading ? "Running Pipeline..." : "Run Review Pipeline"}
        </button>
      </div>

      {error && <div className="error">Error: {error}</div>}
    </div>
  );
}

export default App;

What does this code do?

  • Four useState hooks track the diff text, the pipeline results, a loading flag, and any error messages.
  • runReview sends the diff to your FastAPI backend at http://localhost:8000/api/review as a POST request. It clears previous results, waits for the response, and stores it in state.
  • formatCost formats tiny dollar amounts (like $0.000034) to six decimal places so they are readable.
  • The button is disabled while the pipeline is running or if the textarea is empty. The label changes to "Running Pipeline..." during loading.
  • Save App.jsx.
  • Open your browser to http://localhost:5173. You should see a heading that says "Multi-Model Code Review Pipeline" with a subtitle, a large textarea filled with the sample diff, and a blue "Run Review Pipeline" button.

Seeing a blank page or error?

  • Check your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) for syntax errors. A missing backtick or bracket in the template literal is the most common issue.
  • Make sure the Vite dev server terminal still shows http://localhost:5173 as running.

Help me debug my blank React page

Add the pipeline visualization cards

When the pipeline returns results, the dashboard needs to show what each agent did. You will render three cards (one per agent) showing the provider, model, token counts, cost, and latency.

Below the cards, the raw JSON output from each agent lets you inspect the classification, review, and summary in detail.

  • In App.jsx, find this line near the bottom of the return statement:
      {error && <div className="error">Error: {error}</div>}
    </div>
  • Between the error line and the closing </div>, add the results block with pipeline execution cards and JSON output sections:
      {results && (
        <div className="results">
          <section className="pipeline-section">
            <h2>Pipeline Execution</h2>
            <div className="pipeline-cards">
              {/* Map over the three agents and render a card for each */}
              {results.pipeline.map((agent, i) => (
                <div key={i} className="agent-card">
                  <div className="agent-header">
                    <span className="agent-step">Step {i + 1}</span>
                    <span className="agent-name">{agent.agent}</span>
                  </div>
                  <div className="agent-model">
                    {agent.provider} / {agent.model}
                  </div>
                  <div className="agent-stats">
                    <div>
                      <span className="label">Input</span>
                      <span className="value">{agent.input_tokens} tokens</span>
                    </div>
                    <div>
                      <span className="label">Output</span>
                      <span className="value">
                        {agent.output_tokens} tokens
                      </span>
                    </div>
                    <div>
                      <span className="label">Cost</span>
                      <span className="value">{formatCost(agent.cost)}</span>
                    </div>
                    <div>
                      <span className="label">Latency</span>
                      <span className="value">{agent.latency}s</span>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </section>

          <section className="results-section">
            <h2>Classification (Router Agent)</h2>
            <pre>{JSON.stringify(results.results.classification, null, 2)}</pre>
            <h2>Code Review (Reviewer Agent)</h2>
            <pre>{JSON.stringify(results.results.review, null, 2)}</pre>
            <h2>Final Report (Summarizer Agent)</h2>
            <pre>{JSON.stringify(results.results.summary, null, 2)}</pre>
          </section>
        </div>
      )}

What does this code do?

  • The entire block only renders when results is not null, meaning the pipeline has completed.
  • results.pipeline is the array of three agent objects your backend returns. The .map() iterates over them and renders a card for each with its name, model, token counts, cost, and latency.
  • The results section uses JSON.stringify with 2-space indentation inside <pre> tags to display the raw classification, review, and summary output from each agent.
  • Save App.jsx.
  • Refresh your browser at http://localhost:5173 and click Run Review Pipeline. After 10-20 seconds, you should see three unstyled agent cards appear below the button with token counts and cost data. Below them, three JSON blocks show the classification, review, and final report.

Why does it take 10-20 seconds?

The pipeline runs three API calls in sequence. Each agent waits for the previous one to finish because later agents depend on earlier results. The latency numbers on the cards show you exactly how long each agent took.

Seeing "Error: Failed to fetch" or a server error?

  • Make sure your FastAPI server is still running in the other terminal. If it crashed, restart it with uvicorn main:app --reload from inside the backend/ folder.
  • Check that your API keys are still exported in the backend terminal. If you opened a new terminal, you need to re-export ANTHROPIC_API_KEY, OPENAI_API_KEY, and GEMINI_API_KEY.
  • Look at the FastAPI terminal for error messages. A 401 Unauthorized from any provider means that API key is invalid or missing.

Help me debug my pipeline request from the React frontend

Add the cost comparison panel and styles

The most important insight this dashboard delivers is cost. The cost comparison panel shows the total cost of the multi-model pipeline alongside what it would have cost to run every token through a single model.

This makes the value of model routing visible at a glance. You will also add the full stylesheet to give the dashboard a polished dark theme.

  • In App.jsx, find the closing </section> tag that ends the pipeline-section. It is right after the closing </div> of pipeline-cards.
  • Add the cost comparison section between the pipeline-section closing tag and the results-section opening tag:
          <section className="cost-section">
            <h2>Cost Comparison</h2>
            <div className="cost-comparison">
              {/* Highlight the routed pipeline total cost */}
              <div className="cost-item routed">
                <div className="cost-label">Multi-Model Pipeline (Routed)</div>
                <div className="cost-value">
                  {formatCost(results.totals.total_cost)}
                </div>
                <div className="cost-tokens">
                  {results.totals.input_tokens + results.totals.output_tokens}{" "}
                  total tokens
                </div>
              </div>
              {/* Show what it would cost on each single model */}
              {Object.entries(results.single_model_comparison).map(
                ([model, cost]) => (
                  <div key={model} className="cost-item single">
                    <div className="cost-label">All on {model}</div>
                    <div className="cost-value">{formatCost(cost)}</div>
                    <div className="cost-savings">
                      {cost > results.totals.total_cost
                        ? Math.round(
                            (1 - results.totals.total_cost / cost) * 100
                          ) + "% cheaper with routing"
                        : cost < results.totals.total_cost
                        ? Math.round(
                            (1 - cost / results.totals.total_cost) * 100
                          ) + "% cheaper than routing"
                        : "Same cost"}
                    </div>
                  </div>
                )
              )}
            </div>
          </section>

What does this code do?

  • The routed card at the top shows the actual cost of the multi-model pipeline. It spans the full width to draw attention.
  • Below it, the code maps over single_model_comparison from the API response. This object contains what the same total tokens would have cost if all three agents had used a single model.
  • The savings percentage is calculated dynamically. If routing is cheaper, it shows the percentage saved. If a single cheap model would have been cheaper, it honestly shows that too.
  • Save App.jsx.

You will not see the cost comparison styled properly yet because the CSS file is still empty. That is the next chunk.

  • Open frontend/src/App.css and replace its contents with the full dashboard stylesheet:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    sans-serif;
  background: #0a0a0f;
  color: #e0e0e0;
  min-height: 100vh;
}

/* Main container with max width for readability */
.app {
  max-width: 960px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header h1 {
  font-size: 1.8rem;
  color: #fff;
  margin-bottom: 0.25rem;
}

.subtitle {
  color: #888;
  font-size: 0.95rem;
}

/* Code diff input area */
.input-section {
  margin-bottom: 2rem;
}

.input-section label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: #ccc;
}

textarea {
  width: 100%;
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  color: #e0e0e0;
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: 0.85rem;
  padding: 1rem;
  resize: vertical;
  line-height: 1.5;
}

textarea:focus {
  outline: none;
  border-color: #5b6ef7;
}

button {
  margin-top: 1rem;
  width: 100%;
  padding: 0.75rem;
  background: #5b6ef7;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

button:hover:not(:disabled) {
  background: #4a5ce6;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Error message banner */
.error {
  background: #2a1015;
  border: 1px solid #5a2030;
  color: #f87171;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  margin-bottom: 1.5rem;
}

/* Shared section spacing */
.pipeline-section,
.cost-section,
.results-section {
  margin-bottom: 2rem;
}

h2 {
  font-size: 1.2rem;
  color: #fff;
  margin-bottom: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid #1e1e2e;
}

/* Three-column grid for agent cards */
.pipeline-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.agent-card {
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  padding: 1rem;
}

.agent-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
}

.agent-step {
  font-size: 0.75rem;
  color: #5b6ef7;
  font-weight: 600;
  text-transform: uppercase;
}

.agent-name {
  font-weight: 700;
  color: #fff;
}

.agent-model {
  font-size: 0.8rem;
  color: #888;
  margin-bottom: 0.75rem;
}

.agent-stats > div {
  display: flex;
  justify-content: space-between;
  padding: 0.25rem 0;
  font-size: 0.85rem;
}

.agent-stats .label {
  color: #888;
}

.agent-stats .value {
  color: #e0e0e0;
  font-family: "SF Mono", monospace;
}

/* Two-column grid for cost comparison cards */
.cost-comparison {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}

.cost-item {
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  padding: 1rem;
  text-align: center;
}

/* Routed cost card spans full width with accent border */
.cost-item.routed {
  border-color: #5b6ef7;
  grid-column: 1 / -1;
  background: #141428;
}

.cost-label {
  font-size: 0.85rem;
  color: #888;
  margin-bottom: 0.5rem;
}

.cost-value {
  font-size: 1.5rem;
  font-weight: 700;
  color: #fff;
  font-family: "SF Mono", monospace;
}

.cost-tokens {
  font-size: 0.8rem;
  color: #666;
  margin-top: 0.25rem;
}

.cost-savings {
  font-size: 0.8rem;
  color: #4ade80;
  margin-top: 0.25rem;
}

/* JSON output blocks */
pre {
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  padding: 1rem;
  overflow-x: auto;
  font-size: 0.8rem;
  line-height: 1.5;
  margin-bottom: 1.5rem;
  color: #c0c0d0;
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* Stack cards on small screens */
@media (max-width: 700px) {
  .pipeline-cards {
    grid-template-columns: 1fr;
  }
  .cost-comparison {
    grid-template-columns: 1fr;
  }
}

What does this stylesheet do?

  • Sets a dark background (#0a0a0f) with light text for the entire page.
  • Pipeline cards use a three-column CSS grid. Each card has a dark card background with subtle borders.
  • The routed cost card spans the full width with an accent-colored border (#5b6ef7) to draw attention to the pipeline total.
  • Savings percentages appear in green (#4ade80) for quick visual scanning.
  • The media query at the bottom stacks the cards vertically on screens narrower than 700px.
  • Save App.css.
  • Refresh your browser at http://localhost:5173 and click Run Review Pipeline again. You should see a polished dark dashboard with three styled agent cards in a row, a prominent blue-bordered cost comparison panel below them showing savings percentages in green, and formatted JSON results at the bottom.

Styles not applying?

  • Make sure the import at the top of App.jsx says import "./App.css" with the correct relative path.
  • Check that you saved App.css in the frontend/src/ folder, not the frontend/ root.
  • If index.css still has default Vite styles, those may be overriding your styles. Clear out index.css so it is empty.

Help me debug why my CSS styles are not showing up in Vite

✔️ Awesome, I've got everything!

Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete App.jsx file:

import { useState } from "react";
import "./App.css";

const SAMPLE_DIFF = `--- a/auth.py
+++ b/auth.py
@@ -15,8 +15,12 @@ def authenticate(username, password):
     user = db.query(User).filter_by(username=username).first()
-    if user and user.password == password:
-        return create_token(user)
+    if user is None:
+        return {"error": "User not found"}, 401
+    if not verify_password(password, user.hashed_password):
+        return {"error": "Invalid credentials"}, 401
+    token = create_token(user)
+    log_login(user.id)
+    return {"token": token}, 200
     return None`;

function App() {
  const [codeDiff, setCodeDiff] = useState(SAMPLE_DIFF);
  const [results, setResults] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const runReview = async () => {
    setLoading(true);
    setError(null);
    setResults(null);
    try {
      const response = await fetch("http://localhost:8000/api/review", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ code_diff: codeDiff }),
      });
      if (!response.ok) throw new Error("Server error: " + response.status);
      const data = await response.json();
      setResults(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const formatCost = (cost) => {
    if (cost < 0.01) return "$" + cost.toFixed(6);
    return "$" + cost.toFixed(4);
  };

  return (
    <div className="app">
      <header>
        <h1>Multi-Model Code Review Pipeline</h1>
        <p className="subtitle">
          Three providers. Three agents. One smart pipeline.
        </p>
      </header>

      <div className="input-section">
        <label htmlFor="diff-input">Paste a code diff</label>
        <textarea
          id="diff-input"
          value={codeDiff}
          onChange={(e) => setCodeDiff(e.target.value)}
          rows={15}
          spellCheck={false}
        />
        <button onClick={runReview} disabled={loading || !codeDiff.trim()}>
          {loading ? "Running Pipeline..." : "Run Review Pipeline"}
        </button>
      </div>

      {error && <div className="error">Error: {error}</div>}

      {results && (
        <div className="results">
          <section className="pipeline-section">
            <h2>Pipeline Execution</h2>
            <div className="pipeline-cards">
              {results.pipeline.map((agent, i) => (
                <div key={i} className="agent-card">
                  <div className="agent-header">
                    <span className="agent-step">Step {i + 1}</span>
                    <span className="agent-name">{agent.agent}</span>
                  </div>
                  <div className="agent-model">
                    {agent.provider} / {agent.model}
                  </div>
                  <div className="agent-stats">
                    <div>
                      <span className="label">Input</span>
                      <span className="value">{agent.input_tokens} tokens</span>
                    </div>
                    <div>
                      <span className="label">Output</span>
                      <span className="value">
                        {agent.output_tokens} tokens
                      </span>
                    </div>
                    <div>
                      <span className="label">Cost</span>
                      <span className="value">{formatCost(agent.cost)}</span>
                    </div>
                    <div>
                      <span className="label">Latency</span>
                      <span className="value">{agent.latency}s</span>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </section>

          <section className="cost-section">
            <h2>Cost Comparison</h2>
            <div className="cost-comparison">
              <div className="cost-item routed">
                <div className="cost-label">Multi-Model Pipeline (Routed)</div>
                <div className="cost-value">
                  {formatCost(results.totals.total_cost)}
                </div>
                <div className="cost-tokens">
                  {results.totals.input_tokens + results.totals.output_tokens}{" "}
                  total tokens
                </div>
              </div>
              {Object.entries(results.single_model_comparison).map(
                ([model, cost]) => (
                  <div key={model} className="cost-item single">
                    <div className="cost-label">All on {model}</div>
                    <div className="cost-value">{formatCost(cost)}</div>
                    <div className="cost-savings">
                      {cost > results.totals.total_cost
                        ? Math.round(
                            (1 - results.totals.total_cost / cost) * 100
                          ) + "% cheaper with routing"
                        : cost < results.totals.total_cost
                        ? Math.round(
                            (1 - cost / results.totals.total_cost) * 100
                          ) + "% cheaper than routing"
                        : "Same cost"}
                    </div>
                  </div>
                )
              )}
            </div>
          </section>

          <section className="results-section">
            <h2>Classification (Router Agent)</h2>
            <pre>{JSON.stringify(results.results.classification, null, 2)}</pre>
            <h2>Code Review (Reviewer Agent)</h2>
            <pre>{JSON.stringify(results.results.review, null, 2)}</pre>
            <h2>Final Report (Summarizer Agent)</h2>
            <pre>{JSON.stringify(results.results.summary, null, 2)}</pre>
          </section>
        </div>
      )}
    </div>
  );
}

export default App;

And here is the complete App.css file:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    sans-serif;
  background: #0a0a0f;
  color: #e0e0e0;
  min-height: 100vh;
}

.app {
  max-width: 960px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header h1 {
  font-size: 1.8rem;
  color: #fff;
  margin-bottom: 0.25rem;
}

.subtitle {
  color: #888;
  font-size: 0.95rem;
}

.input-section {
  margin-bottom: 2rem;
}

.input-section label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: #ccc;
}

textarea {
  width: 100%;
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  color: #e0e0e0;
  font-family: "SF Mono", "Fira Code", monospace;
  font-size: 0.85rem;
  padding: 1rem;
  resize: vertical;
  line-height: 1.5;
}

textarea:focus {
  outline: none;
  border-color: #5b6ef7;
}

button {
  margin-top: 1rem;
  width: 100%;
  padding: 0.75rem;
  background: #5b6ef7;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

button:hover:not(:disabled) {
  background: #4a5ce6;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.error {
  background: #2a1015;
  border: 1px solid #5a2030;
  color: #f87171;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  margin-bottom: 1.5rem;
}

.pipeline-section,
.cost-section,
.results-section {
  margin-bottom: 2rem;
}

h2 {
  font-size: 1.2rem;
  color: #fff;
  margin-bottom: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid #1e1e2e;
}

.pipeline-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.agent-card {
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  padding: 1rem;
}

.agent-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
}

.agent-step {
  font-size: 0.75rem;
  color: #5b6ef7;
  font-weight: 600;
  text-transform: uppercase;
}

.agent-name {
  font-weight: 700;
  color: #fff;
}

.agent-model {
  font-size: 0.8rem;
  color: #888;
  margin-bottom: 0.75rem;
}

.agent-stats > div {
  display: flex;
  justify-content: space-between;
  padding: 0.25rem 0;
  font-size: 0.85rem;
}

.agent-stats .label {
  color: #888;
}

.agent-stats .value {
  color: #e0e0e0;
  font-family: "SF Mono", monospace;
}

.cost-comparison {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}

.cost-item {
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  padding: 1rem;
  text-align: center;
}

.cost-item.routed {
  border-color: #5b6ef7;
  grid-column: 1 / -1;
  background: #141428;
}

.cost-label {
  font-size: 0.85rem;
  color: #888;
  margin-bottom: 0.5rem;
}

.cost-value {
  font-size: 1.5rem;
  font-weight: 700;
  color: #fff;
  font-family: "SF Mono", monospace;
}

.cost-tokens {
  font-size: 0.8rem;
  color: #666;
  margin-top: 0.25rem;
}

.cost-savings {
  font-size: 0.8rem;
  color: #4ade80;
  margin-top: 0.25rem;
}

pre {
  background: #141420;
  border: 1px solid #2a2a3a;
  border-radius: 8px;
  padding: 1rem;
  overflow-x: auto;
  font-size: 0.8rem;
  line-height: 1.5;
  margin-bottom: 1.5rem;
  color: #c0c0d0;
  white-space: pre-wrap;
  word-wrap: break-word;
}

@media (max-width: 700px) {
  .pipeline-cards {
    grid-template-columns: 1fr;
  }
  .cost-comparison {
    grid-template-columns: 1fr;
  }
}

Your full-stack multi-model code review system is working end to end. You can paste any code diff, fire it through three specialized agents, and see exactly what each one costs. Next up is the Secret Mission, where you will make the pipeline even smarter by dynamically selecting models based on diff complexity.

Secret mission

Add Dynamic Model Routing

Right now the pipeline always uses the same three models regardless of what it is reviewing. What if a simple style change didn't need the same expensive model as a complex security fix? Upgrade the pipeline so the Router agent's complexity score determines which model handles the review step.

Before you go

Clean Up Your Resources

This project runs entirely on your local machine. API calls to Anthropic, OpenAI, and Google Gemini are pay-per-use, so there are no ongoing costs once you stop making requests.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project directory on your Desktop with backend/ and frontend/ folders.
  • Python virtual environment (venv) with installed packages (anthropic, openai, google-genai, fastapi, uvicorn).
  • React frontend app scaffolded with Vite.
  • Environment variables for three API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment with the pipeline.

  • Your project files, virtual environment, and frontend app stay on your Desktop.
  • Your API keys remain set as environment variables for your current terminal session.
  • No ongoing costs are incurred unless you actively send requests to the APIs.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files stay intact so you can pick up where you left off.

  • Stop the FastAPI server by pressing Ctrl+C in the terminal where uvicorn is running.
  • Stop the Vite dev server by pressing Ctrl+C in the terminal where npm run dev is running.
  • Deactivate the Python virtual environment by running deactivate in your backend terminal.
  • When you come back, reactivate the virtual environment, re-export your API keys, and start the servers again.

Delete - I don't want to use this again

Remove all project files and clean up completely.

  • Stop both servers (FastAPI and Vite) by pressing Ctrl+C in each terminal.
  • Delete the entire project directory from your Desktop.
  • Optionally, revoke or rotate your API keys from the Anthropic, OpenAI, and Google AI Studio dashboards if you no longer need them.
  • Environment variables set with export only last for the current terminal session. Closing your terminal removes them automatically.

Mission accomplished

That's a Wrap!

Nice work! You just built a full-stack multi-model code review pipeline that routes code diffs through three specialized AI agents, each powered by a different provider.

You've learned how to:

  • Built three specialized AI agents using Google Gemini, Anthropic Claude, and OpenAI GPT, each matched to a task based on cost and capability.
  • Implemented context compression between agents, passing structured JSON handoffs instead of raw conversation history to save tokens and sharpen focus.
  • Tracked real token usage and cost per agent in a React dashboard, with a live comparison against single-model alternatives.
  • Added dynamic model routing that adjusts the review model based on the Router agent's complexity score.

Ready to quiz yourself?