Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions solutions/auxen-chatbot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Per-instance base URL issued by the Auxen dashboard
# Looks like https://api.auxen.ai/v1/inst_xxx/v1
AUXEN_API_BASE=

# Per-instance API key issued by the Auxen dashboard (prefix: auxk_)
AUXEN_API_KEY=

# Optional — overrides the default chat model. Must match the model your
# Auxen instance is provisioned to serve. Defaults to llama-3.1-8b.
# AUXEN_MODEL=llama-3.1-8b
9 changes: 9 additions & 0 deletions solutions/auxen-chatbot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.next
out
.DS_Store
.env
.env.local
.env.*.local
*.tsbuildinfo
next-env.d.ts
81 changes: 81 additions & 0 deletions solutions/auxen-chatbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: Auxen Dedicated LLM Chatbot
slug: auxen-chatbot
publisher: Auxen
description: Next.js chatbot wired to your own dedicated LLM endpoint on Auxen. Per-minute GPU billing, no per-token fees, OpenAI-compatible API.
framework: Next.js
useCase: AI
css: Tailwind
deployUrl: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fauxen-ai%2Fauxen-nextjs-starter&env=AUXEN_API_BASE,AUXEN_API_KEY&envDescription=Per-instance%20base%20URL%20and%20API%20key%20issued%20by%20the%20Auxen%20dashboard&envLink=https%3A%2F%2Fauxen.ai
demoUrl: https://auxen.ai
---

# Auxen Dedicated LLM Chatbot

A minimal Next.js chatbot wired to your own [Auxen](https://auxen.ai) dedicated LLM endpoint. Zero external SDK dependencies — just `fetch` against Auxen's OpenAI-compatible `/v1/chat/completions` API.

## What is Auxen

[Auxen](https://auxen.ai) hosts per-customer **dedicated** LLM endpoints (Llama 3.1/3.2, Qwen 2.5, Mistral, Gemma 2, Mixtral, Phi-3, Command R). Each instance is a dedicated GPU running one open-source model, billed per-minute of runtime — no per-token charges, no monthly minimums.

## Demo

The chatbot runs in your own browser against your own Auxen instance — provision one at [auxen.ai](https://auxen.ai) to try it.

## How to Use

You can choose from one of the following two methods to use this repository:

### One-Click Deploy

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fauxen-ai%2Fauxen-nextjs-starter&env=AUXEN_API_BASE,AUXEN_API_KEY&envDescription=Per-instance%20base%20URL%20and%20API%20key%20issued%20by%20the%20Auxen%20dashboard&envLink=https%3A%2F%2Fauxen.ai)

### Clone and Deploy

```bash
git clone https://github.com/auxen-ai/auxen-nextjs-starter.git
cd auxen-nextjs-starter
npm install
cp .env.example .env.local # then fill in AUXEN_API_BASE and AUXEN_API_KEY
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) and start chatting.

## Configuration

| Env var | Description |
| ---------------- | -------------------------------------------------------------------------------------- |
| `AUXEN_API_BASE` | Per-instance base URL, e.g. `https://api.auxen.ai/v1/inst_xxx/v1` (from the dashboard) |
| `AUXEN_API_KEY` | Per-instance API key prefixed `auxk_` (from the dashboard) |
| `AUXEN_MODEL` | Optional — the model your instance is serving. Defaults to `llama-3.1-8b`. |

## How it works

The Edge route handler at `/api/chat` proxies messages to your Auxen instance and streams the OpenAI Chat Completions SSE response back to the browser:

```ts
const upstream = await fetch(`${AUXEN_API_BASE}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${AUXEN_API_KEY}`,
},
body: JSON.stringify({ model, messages, stream: true }),
});
return new Response(upstream.body, {
headers: { "Content-Type": "text/event-stream" },
});
```

That's the whole integration. No AI SDK dependency — `fetch` + the browser's SSE parser handle everything.

## Pricing

Auxen bills per-minute of dedicated GPU runtime, not per token. See [auxen.ai/pricing](https://auxen.ai/pricing).

## Source

Canonical source repo: [github.com/auxen-ai/auxen-nextjs-starter](https://github.com/auxen-ai/auxen-nextjs-starter)
54 changes: 54 additions & 0 deletions solutions/auxen-chatbot/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Minimal proxy from the browser to your Auxen instance. Auxen exposes the
// OpenAI Chat Completions wire format on every instance, so the only thing
// this route does is forward the messages, add the bearer token, and stream
// the SSE response back to the client unchanged.

export const runtime = "edge";
export const maxDuration = 30;

interface ChatRequest {
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
}

export async function POST(req: Request) {
const { messages }: ChatRequest = await req.json();

const base = process.env.AUXEN_API_BASE;
const key = process.env.AUXEN_API_KEY;
const model = process.env.AUXEN_MODEL ?? "llama-3.1-8b";

if (!base || !key) {
return new Response(
JSON.stringify({
error:
"Auxen credentials missing. Set AUXEN_API_BASE and AUXEN_API_KEY env vars (get them from https://auxen.ai).",
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}

const upstream = await fetch(`${base.replace(/\/$/, "")}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${key}`,
},
body: JSON.stringify({ model, messages, stream: true }),
});

if (!upstream.ok || !upstream.body) {
const text = await upstream.text();
return new Response(
JSON.stringify({ error: text || `Auxen returned ${upstream.status}` }),
{ status: upstream.status, headers: { "Content-Type": "application/json" } },
);
}

return new Response(upstream.body, {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
10 changes: 10 additions & 0 deletions solutions/auxen-chatbot/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

html, body { height: 100%; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f5f5f5;
}
20 changes: 20 additions & 0 deletions solutions/auxen-chatbot/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "Auxen Chat Starter",
description:
"Next.js + Vercel AI SDK chatbot starter wired to an Auxen dedicated LLM endpoint.",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
136 changes: 136 additions & 0 deletions solutions/auxen-chatbot/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use client";

import { useState, type FormEvent } from "react";

type Message = { role: "user" | "assistant"; content: string };

export default function Page() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);

async function handleSubmit(e: FormEvent) {
e.preventDefault();
const text = input.trim();
if (!text || isLoading) return;

const userMsg: Message = { role: "user", content: text };
const next = [...messages, userMsg];
setMessages([...next, { role: "assistant", content: "" }]);
setInput("");
setIsLoading(true);

try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: next }),
});
if (!res.ok || !res.body) {
const err = await res.text();
setMessages([
...next,
{ role: "assistant", content: `Error: ${err.slice(0, 300)}` },
]);
return;
}

const reader = res.body.getReader();
const decoder = new TextDecoder();
let assistantText = "";
let buffer = "";

while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });

const lines = buffer.split("\n");
buffer = lines.pop() ?? "";

for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const data = trimmed.slice(5).trim();
if (data === "[DONE]") continue;
try {
const chunk = JSON.parse(data);
const delta = chunk.choices?.[0]?.delta?.content;
if (typeof delta === "string") {
assistantText += delta;
setMessages((prev) => {
const copy = prev.slice();
copy[copy.length - 1] = {
role: "assistant",
content: assistantText,
};
return copy;
});
}
} catch {
// ignore parse errors on partial chunks
}
}
}
} finally {
setIsLoading(false);
}
}

return (
<main className="flex min-h-screen flex-col items-center justify-between p-6 max-w-2xl mx-auto">
<div className="w-full">
<header className="mb-8">
<h1 className="text-2xl font-semibold">Auxen Chat</h1>
<p className="text-sm text-neutral-400 mt-1">
Next.js + your own dedicated LLM endpoint on Auxen
</p>
</header>

<div className="flex flex-col gap-4 mb-6">
{messages.length === 0 ? (
<p className="text-neutral-500 text-sm">
Set <code className="text-neutral-300">AUXEN_API_BASE</code> and{" "}
<code className="text-neutral-300">AUXEN_API_KEY</code> in your
environment, then say hi.
</p>
) : (
messages.map((m, i) => (
<div
key={i}
className="flex flex-col gap-1 rounded-lg border border-neutral-800 p-3 bg-neutral-900"
>
<span className="text-[11px] uppercase tracking-wide text-neutral-500">
{m.role}
</span>
<span className="whitespace-pre-wrap text-sm text-neutral-100">
{m.content || (m.role === "assistant" && isLoading ? "…" : "")}
</span>
</div>
))
)}
</div>

<form
onSubmit={handleSubmit}
className="flex gap-2 sticky bottom-0 bg-[#0a0a0a] py-2"
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Send a message…"
className="flex-1 rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm focus:outline-none focus:border-neutral-600"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="rounded-lg bg-white text-black px-4 py-2 text-sm font-medium disabled:opacity-40"
>
Send
</button>
</form>
</div>
</main>
);
}
6 changes: 6 additions & 0 deletions solutions/auxen-chatbot/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

export default nextConfig;
30 changes: 30 additions & 0 deletions solutions/auxen-chatbot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "auxen-nextjs-starter",
"version": "0.1.0",
"private": true,
"description": "Next.js chatbot starter wired to an Auxen dedicated LLM endpoint via the OpenAI-compatible /v1/chat/completions API.",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^15.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.4.0"
},
"engines": {
"node": ">=18"
}
}
3 changes: 3 additions & 0 deletions solutions/auxen-chatbot/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
plugins: { tailwindcss: {}, autoprefixer: {} },
};
9 changes: 9 additions & 0 deletions solutions/auxen-chatbot/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Config } from "tailwindcss";

const config: Config = {
content: ["./app/**/*.{ts,tsx}"],
theme: { extend: {} },
plugins: [],
};

export default config;
Loading