Building a django chatbot gpt-4o claude integration doesn’t have to mean maintaining two separate API clients and request shapes. This tutorial builds a minimal but realistic Django app that exposes one chat interface and dispatches to either model behind a small abstraction. You’ll get runnable code, schema, and expected outputs at each checkpoint.
Prerequisites
- Python 3.11 or newer
- Django 5.0+ (
pip install django) openaiandanthropicPython packages (pip install openai anthropic)- API keys for OpenAI and Anthropic (set as
OPENAI_API_KEY,ANTHROPIC_API_KEY) - Comfort with Django’s MVT basics
If you prefer a single endpoint, an OpenAI-compatible gateway such as n4n.ai addresses 240+ models behind one base URL and handles fallback when a provider is rate-limited, but the code below uses the native SDKs so you see the raw shapes.
Scaffold the project
django-admin startproject chatproject
cd chatproject
python manage.py startapp chat
Add 'chat' to INSTALLED_APPS in chatproject/settings.py. Run python manage.py migrate to confirm the DB works. You should see a clean migration with no errors.
Expected checkpoint output:
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
...
Project layout after the next steps:
chatproject/
chat/
models.py
views.py
llm.py
urls.py
templates/chat/
chatproject/
settings.py
urls.py
Data model
We store conversations and messages so the chatbot has memory across HTTP requests. Edit chat/models.py:
from django.db import models
class Conversation(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
model = models.CharField(max_length=64, default="gpt-4o")
class Message(models.Model):
ROLE_CHOICES = [("user", "user"), ("assistant", "assistant")]
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name="messages")
role = models.CharField(max_length=16, choices=ROLE_CHOICES)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
Migrate:
python manage.py makemigrations chat
python manage.py migrate
Expected tail of output:
Applying chat.0001_initial... OK
Why a unified abstraction matters
GPT-4o and Claude have different request schemas: OpenAI uses messages with a system role; Anthropic uses a top-level system string and expects strictly alternating user/assistant turns. If you scatter provider calls across views, you’ll duplicate role-normalization logic and make model switching painful. A single complete() function keeps views dumb and lets you swap providers or add a third without touching templates.
Unified client abstraction
Create chat/llm.py. This wraps both providers and returns plain text.
import os
from openai import OpenAI
import anthropic
OPENAI_MODELS = {"gpt-4o", "gpt-4o-mini"}
CLAUDE_MODELS = {"claude-3-5-sonnet-20240620", "claude-3-opus-20240229"}
def complete(model: str, history: list[dict]) -> str:
if model in OPENAI_MODELS:
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
resp = client.chat.completions.create(model=model, messages=history)
return resp.choices[0].message.content
elif model in CLAUDE_MODELS:
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# Anthropic expects alternating user/assistant; drop system for brevity
msgs = [{"role": m["role"], "content": m["content"]} for m in history if m["role"] != "system"]
resp = client.messages.create(model=model, max_tokens=1024, messages=msgs)
return resp.content[0].text
else:
raise ValueError(f"Unsupported model: {model}")
If you route through a unified gateway, you could replace both branches with a single OpenAI(base_url="https://api.n4n.ai/v1") call and pass model strings like openai/gpt-4o or anthropic/claude-3-5-sonnet. The gateway forwards cache-control hints and meters per token, but the native split above is clearer for learning the differences.
Views and URLs
We’ll build a view that renders a conversation and accepts POSTed user input. In chat/views.py:
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponseRedirect
from .models import Conversation, Message
from .llm import complete
def index(request):
conversations = Conversation.objects.all().order_by("-created_at")
return render(request, "chat/index.html", {"conversations": conversations})
def conversation_detail(request, pk):
conv = get_object_or_404(Conversation, pk=pk)
if request.method == "POST":
user_text = request.POST["text"]
Message.objects.create(conversation=conv, role="user", content=user_text)
history = [{"role": m.role, "content": m.content} for m in conv.messages.all()]
try:
reply = complete(conv.model, history)
except Exception as e:
reply = f"Error: {e}"
Message.objects.create(conversation=conv, role="assistant", content=reply)
return HttpResponseRedirect(request.path)
messages = conv.messages.all()
return render(request, "chat/detail.html", {"conv": conv, "messages": messages})
def new_conversation(request):
if request.method == "POST":
model = request.POST.get("model", "gpt-4o")
conv = Conversation.objects.create(model=model)
return redirect("conversation_detail", pk=conv.pk)
return render(request, "chat/new.html")
Wire URLs in chat/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("new/", views.new_conversation, name="new_conversation"),
path("c/<int:pk>/", views.conversation_detail, name="conversation_detail"),
]
Include them in the root chatproject/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("chat.urls")),
]
Templates
Create chat/templates/chat/index.html:
<h1>Conversations</h1>
<a href="{% url 'new_conversation' %}">New</a>
<ul>
{% for c in conversations %}
<li><a href="{% url 'conversation_detail' c.pk %}">{{ c.model }} — {{ c.created_at }}</a></li>
{% endfor %}
</ul>
new.html:
<form method="post">
{% csrf_token %}
<select name="model">
<option value="gpt-4o">GPT-4o</option>
<option value="claude-3-5-sonnet-20240620">Claude 3.5 Sonnet</option>
</select>
<button type="submit">Create</button>
</form>
detail.html:
<h1>{{ conv.model }}</h1>
<div id="messages">
{% for m in messages %}
<p><strong>{{ m.role }}:</strong> {{ m.content }}</p>
{% endfor %}
</div>
<form method="post">
{% csrf_token %}
<textarea name="text" rows="3"></textarea>
<button type="submit">Send</button>
</form>
Run and verify
Start the dev server:
python manage.py runserver
Open http://127.0.0.1:8000/new/, pick gpt-4o, submit. You’ll be redirected to /c/1/. Type “Explain Django middleware in one sentence.” Submit.
Expected assistant message (truncated):
assistant: Django middleware is a lightweight plugin that processes requests and responses globally before they reach views or after they leave them.
Create another conversation with Claude. Same prompt yields a different phrasing but same intent. The django chatbot gpt-4o claude pair now shares one UI and one storage layer.
You can also hit the flow with curl to confirm non-browser behavior:
curl -s -X POST http://127.0.0.1:8000/c/1/ -d "text=Hello" --cookie-jar c.txt --cookie c.txt
The HTML response will contain the new <p> blocks.
Handling streaming
Non-streaming blocks the request until the full completion returns. For production, use StreamingHttpResponse. With OpenAI:
from django.http import StreamingHttpResponse
def stream_complete(model, history):
client = OpenAI()
stream = client.chat.completions.create(model=model, messages=history, stream=True)
for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
Anthropic supports stream=True on messages.create with similar iteration. Wire the generator into StreamingHttpResponse so the django chatbot gpt-4o claude UI updates token by token instead of freezing for seconds.
Operational notes
- Store API keys in environment variables or a secrets manager; never commit them.
- Claude requires
max_tokens; OpenAI has sensible defaults but set your own limits. - For multi-turn Claude, enforce alternating roles or the SDK raises a 400. OpenAI is lenient.
- Move
complete()calls to a Celery task if you expect concurrent users; Django’s dev server is single-threaded by default. - The abstraction above is intentionally thin. Add retry with backoff, token counting, and request logging before shipping.
That’s a working django chatbot gpt-4o claude baseline. From here, add authentication, per-user quotas, and a proper frontend if you need more than a demo.