dánwòdánwò

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:

danwo.config.ts
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

FieldWhat it seeds
scriptsReplies, and the calls each one answers; the first that matches wins. See Scripting replies.
unscriptedWhat a call no script answers gets: its own words back, marked [danwo: unscripted] (echo, the default), or a 500 (fail).
pricesDollars per million input and output tokens, by model, for cost.
modelsThe models GET /v1/models lists.
knownModelsOnlyAnswer a model not in models with a 404, as the gateway does.
recordRecord 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:

tests/summary.danwo.ts
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 callWhat it holds
provider, modelWhich API and model the app called.
system, messages, toolsThe prompt, and the tools offered.
reply, scriptThe answer, and the script that gave it.
usage, costTokens 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

APICovered
OpenAIChat Completions and Responses, streaming, tools, structured output, embeddings.
AnthropicMessages, streaming, tools and token counting.
AI GatewayThe AI SDK's language and embedding models, with cost metadata.
Gemini, Perplexity, BedrockSee More model APIs.

Faults

CallFaults the matrix tries
A model call, such as POST /v1/chat/completions429, 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 emulatora 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

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI