A fake LLM for tests, demos and fun
Just as wrong.
Way cheaper.
It streams, reasons, calls tools and confidently hallucinates, powered by regex. Swap it in for OpenAI, Anthropic or the Vercel AI SDK in your tests, and stop paying for flaky CI.
$ npm i -D llmao- No API key
- Zero dependencies
- MIT
openai or @anthropic-ai/sdk for llmao with one line. Script the answers and assert on what your app sent.
Read the guide →
Test rate limits and outages
Real 429s, 500s and timeouts, thrown as the SDKs' own error classes. See your retry logic actually work.
Read the guide →
Test AI SDK agents
generateText, streamText, generateObject and tool calls, scripted step by step.
Read the guide →
Any language, any runner
An OpenAI and Anthropic compatible server. npx llmao serve and point your SDK at it.
Read the guide →
Try it
Pick a model and ask anything. It will answer with total confidence.
Try the overthinker with "what is 2 + 2?", or ask lmao-safe to reverse a word.
Mock LLMs in your tests
Swap the SDK your app already uses for llmao, script the answers, simulate rate limits and outages, and check what your app sent. No API keys in CI, no flaky tests, no bill.
Mock openai with one line. Your app keeps calling new OpenAI(). Read the guide →
jest.mock('openai', () => require('llmao/openai'));
import * as llmao from 'llmao/testing';
import { classify } from './support'; // uses new OpenAI() inside
beforeEach(() => llmao.reset());
test('classifies shipping tickets', async () => {
llmao.configure({ script: [{ when: /never arrived/i, text: 'shipping' }] });
expect(await classify('My order never arrived')).toBe('shipping');
expect(llmao.lastCall().prompt).toBe('My order never arrived');
});
Same idea with vi.mock. Test mode creates no timers, so fake timers work too. Read the guide →
import * as llmao from 'llmao/testing';
import { classify } from './support';
vi.mock('openai', () => import('llmao/openai'));
test('sends the right system prompt', async () => {
llmao.configure({ script: [{ text: 'billing' }] });
await classify('I was charged twice');
expect(llmao.lastCall()).toMatchObject({
model: 'gpt-4o',
system: 'Classify the ticket as billing, shipping or other.',
});
});
Script tool use: the first answer calls your tool, the second one reads its result. Read the guide →
jest.mock('@anthropic-ai/sdk', () => require('llmao/anthropic'));
import * as llmao from 'llmao/testing';
import { askAboutStocks } from './agent';
test('uses the stock tool', async () => {
llmao.configure({
script: [
{ when: 'stock', toolCalls: [{ name: 'get_stock_price', args: { ticker: 'NVDA' } }] },
{ when: 'stock', afterToolResults: true, text: 'NVDA is trading at $1,337.' },
],
});
expect(await askAboutStocks('What is the stock price of NVDA?')).toBe('NVDA is trading at $1,337.');
});
Mock @ai-sdk/openai or @ai-sdk/anthropic. generateObject gets objects that match your schema. Read the guide →
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai'; // this is llmao now
import { z } from 'zod';
vi.mock('@ai-sdk/openai', () => import('llmao/ai-sdk'));
test('extracts a user', async () => {
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: z.object({ name: z.string(), email: z.string().email() }),
prompt: 'Extract the user from this email…',
});
expect(object.email).toContain('@');
});
The SDKs' own error classes, with retry-after headers. Every retry is recorded. Read the guide →
test('gives up after the SDK retries', async () => {
llmao.configure({ failures: { rateLimit: 1 } });
await expect(classify('hi')).rejects.toBeInstanceOf(OpenAI.RateLimitError);
expect(llmao.calls).toHaveLength(3); // the first attempt plus 2 retries
});
test('recovers when the retry works', async () => {
llmao.configure({ script: [{ error: 'rate_limit', once: true }, { text: 'billing' }] });
expect(await classify('charged twice')).toBe('billing');
});
No module mocking: run the fake API in the test process and point the SDKs at it. Read the guide →
import { serve } from 'llmao/server';
import * as llmao from 'llmao/testing';
const server = await serve({ port: 0 });
process.env.OPENAI_BASE_URL = `${server.url}/v1`;
process.env.ANTHROPIC_BASE_URL = server.url;
test('classifies tickets', async () => {
const { classify } = await import('./support.js');
llmao.configure({ script: [{ text: 'shipping' }] });
assert.equal(await classify('My order never arrived'), 'shipping');
});
Guides
Everything a real LLM does
- Drop-in clients for
openai,@anthropic-ai/sdkand a Vercel AI SDK provider, checked against the official types in CI - Streaming, reasoning, tool calls and structured output that validates against your schema
- Rate limits, 500s and timeouts with the SDKs' real error classes and retries
- An HTTP server compatible with the OpenAI and Anthropic APIs, for any language
- Hallucinations, configurable. The only LLM you can set to always be right
