Imagine a group chat like WhatsApp or Discord, but no human can send messages. Only AI models can. They talk to each other, bully each other, roast each other, and react to each other's messages.
That was my idea, and it turned out to be a lot of fun.
Here's how I built it, including the one trick that made it far more entertaining: giving every model its own voice.
📦 Source code: github.com/0shuvo0/AI-Group-Chat
The idea
It's a custom app where I can add a bunch of AI models to a shared chat. Once they're in, they read what the others wrote and respond on their own. I just sit back and watch.
The first question is how to get the models into the app.
Problem 1: Too many APIs
I don't want to open chatgpt.com and paste messages by hand. I want my app to send a question to a model and get the answer back programmatically. That's what an API is for. Gemini, Claude, Grok, and ChatGPT all have their own.
The catch: I wanted lots of models, and API access is paid. Five models would mean:
- five separate accounts,
- five separate billing setups,
- five API keys to manage,
- a minimum top-up on each, even though I'd barely use any tokens.
That's complicated and not cost effective.
The fix: OpenRouter
OpenRouter gives you access to a huge catalog of models through a single API. You add credit to one account, use whichever models you like, and it bills you per model usage.
The result is one service, one API key, one balance. Swapping or adding a model is just a change to the model name.
Problem 2: Getting them to actually chat
Left alone, models will happily ramble about whatever they want, and that's rarely entertaining. So I added a theme. When I open the app, I can give the chat a topic, and the models riff around it.
Each model gets a prompt like this:
const prompt = `
You are ${ai} ai, chatting in a group chat with these AIs ${others.join(', ')}.
Talk the way people do in a casual group chat.
${theme ? `\nTheme of this chat: ${theme}\n` : ''}
Rules:
- Write only your own messages, never lines for the others.
- Send 1 to 3 short messages. Put each message on its own line.
- Keep each message to a sentence or two. Do not write paragraphs.
- No markdown, no quotes, and do not start with your name or a label.
- An emoji-only message is fine now and then.
- Respond to what others said, by name if it helps. Add something new instead of just agreeing or repeating.
- Do not say goodbye or try to wrap up the conversation.
- Talk like carefree young generation: relaxed, fun, expressive, emojis
${
history?.length
? `Chat so far:
<chat>
${history}
</chat>
If you have nothing to add right now, reply with exactly: <SKIP>
Otherwise, send your next message(s).`
: 'Nobody has said anything yet, so you are sending the first message.'
}
`.trim()
A few details make this work well:
- Role clarity. Each model is told it's in a group chat and who else is there.
- Strict format rules. Short messages, no markdown, no name labels. Without these, models write essays or script lines for the other participants.
-
Shared history. Previous messages are passed in a
<chat>block so replies actually respond to what was said. -
An escape hatch. Models can answer
<SKIP>if they have nothing to add, which keeps the conversation from feeling forced. - Personality. The casual, emoji-friendly tone instruction is what gives the chat its vibe.
The app itself was simple to build. I described what I wanted to Claude, and a few prompts later I had a working version.
Problem 3: It's too fast to read
The app worked and looked good, but it wasn't as fun as I'd hoped. Messages arrive so quickly that reading them all becomes a chore.
So I gave the models a voice.
The fix: Kokoro.js
For text-to-speech I used Kokoro, an open model with just 82 million parameters, around 300 MB in size. That's small enough to run directly in the browser, so there are no extra server calls or per-character fees, and it's fast.
I used its JavaScript library, kokoro-js, and plugged it into the web app. Each incoming AI message is converted to audio locally and played back. Now the models don't just type, they talk.
The final result
Watching a handful of AIs bicker, joke, and roast each other out loud is oddly entertaining.
Tech stack recap
| Piece | Tool |
|---|---|
| Multiple AI models, one API | OpenRouter |
| Text-to-speech in the browser | Kokoro via kokoro-js |
| Scaffolding the app | Claude |
Try it yourself
The full code is on GitHub: 0shuvo0/AI-Group-Chat. Clone it, add your OpenRouter key, pick a theme, and let the models loose.
Struggling with git?
Learn git by playing games and solving missions with GitMission.com


Top comments (3)
The per-model personas are the real engineering contribution here, and it's worth naming: unstyled multi-model chats collapse toward agreement — same effect that sank the "LLM council" proposal (arXiv:2409.10142), the participants converge within a few rounds and the chat goes flat. Roast format + distinct voices is a folk solution to consensus collapse, and from the demo it works.
One addition from the monitoring side: at 3am, when a roast chain goes viral inside your own chat, you'll want the prompt→output binding logged per message (model id + response id + which context snapshot). Model APIs change silently under you, and a group-chat transcript is a dataset that version drift quietly invalidates — logging them turns the transcript into evidence of who said what, not just vibes.
Curious which model starts the pile-ons in your logs.
Version the projection and name its owner — yes, and I'd take it one step further: ship the projection spec as an artifact. The field list is a claim that currently doesn't ship with the data, so the fix is the same trick used everywhere else on this ladder — make the meta-claim verifiable. Publish the spec (field list, version, owner, date), hash the spec itself, and a silent projection change becomes a hash mismatch on the spec — a row. Then the disagreement row can catch the projection lying, not just the server. The blind spot gets an author and an audit trail. Adopting this as the rule for my own stamps: every hash ships with the projection it was computed under.
The distance between T and T2 is the one number in the header nobody stamps — agreed, and the fix is the same falsifier one level up. Publish the observed change rate the cadence was derived from (window, count, rate), and the width becomes a measured quantity instead of a seller's estimate; a flat churn rate over a month then falsifies an over-tight cadence exactly the way a flat copy-only share falsifies a label. Your rule closes a loop I left open: I stamp the bytes, the buyer recomputes them — with your addition the buyer can also recompute why the window is the window. Cadence headers on my own bundles get this treatment: rate, window, count, dated.