diff --git a/solutions/auxen-chatbot/.env.example b/solutions/auxen-chatbot/.env.example new file mode 100644 index 0000000000..d8dbe806cd --- /dev/null +++ b/solutions/auxen-chatbot/.env.example @@ -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 diff --git a/solutions/auxen-chatbot/.gitignore b/solutions/auxen-chatbot/.gitignore new file mode 100644 index 0000000000..e60aa4454d --- /dev/null +++ b/solutions/auxen-chatbot/.gitignore @@ -0,0 +1,9 @@ +node_modules +.next +out +.DS_Store +.env +.env.local +.env.*.local +*.tsbuildinfo +next-env.d.ts diff --git a/solutions/auxen-chatbot/README.md b/solutions/auxen-chatbot/README.md new file mode 100644 index 0000000000..0781a16948 --- /dev/null +++ b/solutions/auxen-chatbot/README.md @@ -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) diff --git a/solutions/auxen-chatbot/app/api/chat/route.ts b/solutions/auxen-chatbot/app/api/chat/route.ts new file mode 100644 index 0000000000..3ea745a024 --- /dev/null +++ b/solutions/auxen-chatbot/app/api/chat/route.ts @@ -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", + }, + }); +} diff --git a/solutions/auxen-chatbot/app/globals.css b/solutions/auxen-chatbot/app/globals.css new file mode 100644 index 0000000000..e9c248d06e --- /dev/null +++ b/solutions/auxen-chatbot/app/globals.css @@ -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; +} diff --git a/solutions/auxen-chatbot/app/layout.tsx b/solutions/auxen-chatbot/app/layout.tsx new file mode 100644 index 0000000000..cf847d1939 --- /dev/null +++ b/solutions/auxen-chatbot/app/layout.tsx @@ -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 ( + + {children} + + ); +} diff --git a/solutions/auxen-chatbot/app/page.tsx b/solutions/auxen-chatbot/app/page.tsx new file mode 100644 index 0000000000..4bce76d0b2 --- /dev/null +++ b/solutions/auxen-chatbot/app/page.tsx @@ -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([]); + 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 ( +
+
+
+

Auxen Chat

+

+ Next.js + your own dedicated LLM endpoint on Auxen +

+
+ +
+ {messages.length === 0 ? ( +

+ Set AUXEN_API_BASE and{" "} + AUXEN_API_KEY in your + environment, then say hi. +

+ ) : ( + messages.map((m, i) => ( +
+ + {m.role} + + + {m.content || (m.role === "assistant" && isLoading ? "…" : "")} + +
+ )) + )} +
+ +
+ 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} + /> + +
+
+
+ ); +} diff --git a/solutions/auxen-chatbot/next.config.mjs b/solutions/auxen-chatbot/next.config.mjs new file mode 100644 index 0000000000..d5456a15d4 --- /dev/null +++ b/solutions/auxen-chatbot/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/solutions/auxen-chatbot/package.json b/solutions/auxen-chatbot/package.json new file mode 100644 index 0000000000..6840f1bc2f --- /dev/null +++ b/solutions/auxen-chatbot/package.json @@ -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" + } +} diff --git a/solutions/auxen-chatbot/postcss.config.mjs b/solutions/auxen-chatbot/postcss.config.mjs new file mode 100644 index 0000000000..e008c9ceed --- /dev/null +++ b/solutions/auxen-chatbot/postcss.config.mjs @@ -0,0 +1,3 @@ +export default { + plugins: { tailwindcss: {}, autoprefixer: {} }, +}; diff --git a/solutions/auxen-chatbot/tailwind.config.ts b/solutions/auxen-chatbot/tailwind.config.ts new file mode 100644 index 0000000000..61f5fe2f84 --- /dev/null +++ b/solutions/auxen-chatbot/tailwind.config.ts @@ -0,0 +1,9 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{ts,tsx}"], + theme: { extend: {} }, + plugins: [], +}; + +export default config; diff --git a/solutions/auxen-chatbot/tsconfig.json b/solutions/auxen-chatbot/tsconfig.json new file mode 100644 index 0000000000..25a72b4954 --- /dev/null +++ b/solutions/auxen-chatbot/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}