AI
Model APIs
The model APIs your app calls, OpenAI, Anthropic, the Vercel AI Gateway, Gemini, Perplexity and Bedrock, answered from scripts, with token counts and cost a test can check.
danwo's model API emulator answers the APIs your app's AI features call: OpenAI, Anthropic, the
Vercel AI Gateway, Gemini, Perplexity and Bedrock. The openai and @anthropic-ai/sdk packages
and the AI SDK work against it unchanged, streaming or not.
Each call is answered from a script you write, so a test knows exactly what the model will say. That makes AI features testable like any other: same input, same answer, every run.
This page covers setting it up and reading the calls back. Writing replies is on Scripting replies, and Gemini, Perplexity and Bedrock on More model APIs.
Set up
Add llmService with a script and the prices you want cost reported at. Any key is accepted, so
your app keeps its own:
import { appEnvironment, defineConfig, llmService } from "@danwo/sdk"
export default defineConfig({
testDir: "tests",
environment: () =>
appEnvironment({
app: { command: "npm run dev", url: "http://localhost:3000" },
services: [
llmService({
seed: {
scripts: [
{
name: "summary",
match: { user: /summari[sz]e/ },
reply: { text: "Three orders shipped late this week." },
},
],
prices: { "gpt-5": { input: 1.25, output: 10 } },
},
}),
],
}),
})Seed it
| Field | What it seeds |
|---|---|
scripts | Replies, and the calls each one answers; the first that matches wins. See Scripting replies. |
unscripted | What a call no script answers gets: its own words back, marked [danwo: unscripted] (echo, the default), or a 500 (fail). |
prices | Dollars per million input and output tokens, by model, for cost. |
models | The models GET /v1/models lists. |
knownModelsOnly | Answer a model not in models with a 404, as the gateway does. |
record | Record unscripted calls from the real provider with your own key, then replay them. |
Use it in a test
Ask for a summary, then check the call the app made and what it cost:
import { type LlmFixtures, llmCalls, test as base } from "@danwo/sdk"
const test = base.extend<LlmFixtures>()
test("summarises the week in one call", async ({ app, screen, expect, llm }) => {
await app.open("/reports")
await screen.getByRole("button", { name: "Summarise this week" }).click()
await expect(screen.getByText("Three orders shipped late this week.")).toBeVisible()
const calls = llmCalls(llm)
expect(calls.all()).toHaveLength(1)
expect(calls.last()?.model).toBe("gpt-5")
expect(calls.cost()).toBeLessThan(0.01)
})| On each call | What it holds |
|---|---|
provider, model | Which API and model the app called. |
system, messages, tools | The prompt, and the tools offered. |
reply, script | The answer, and the script that gave it. |
usage, cost | Tokens in and out, and dollars. |
Cost
Token counts are an estimate, about four characters a token, and the same for the same text
every time, so a test of your app's usage tracking can check exact numbers. With prices seeded,
each call's cost is in dollars, and the AI Gateway's answers carry that cost in their provider
metadata, as the real gateway's do.
What's covered
| API | Covered |
|---|---|
| OpenAI | Chat Completions and Responses, streaming, tools, structured output, embeddings. |
| Anthropic | Messages, streaming, tools and token counting. |
| AI Gateway | The AI SDK's language and embedding models, with cost metadata. |
| Gemini, Perplexity, Bedrock | See More model APIs. |
Faults
| Call | Faults the matrix tries |
|---|---|
A model call, such as POST /v1/chat/completions | 429, 500, 503 (529 on Anthropic), a 400 for a context that's too long, a delay, connection reset, answer lost after the model answered |
| The same, shaped by the emulator | a refusal, an empty completion, a truncated answer, a content filter, a malformed tool call, a stream cut off halfway |
The last row is what only an emulator can do: the real API won't refuse on cue.
Fidelity
Live for the AI Gateway. danwo fidelity gateway runs 6 scenarios against the real
gateway, from text and tool calls to streams, embeddings and errors. All 6 match.
Gemini, Perplexity and Bedrock are checked offline, against their published descriptions. OpenAI and Anthropic have no suite of their own yet.
Not covered yet
- Images, audio and video
- OpenAI's Assistants, Realtime and batch APIs
- Fine-tuning and file uploads
Next
S3 and Tigris
S3 object storage for apps on AWS S3 or Tigris, with the AWS SDK and the Tigris SDK, presigned URLs, multipart uploads and signature checks.
Scripting replies
Script what the model answers your app, text, tool calls, structured output or a refusal, matched by prompt, model, provider or tool, and change it mid-test.