Give your model the ability to read any page on the web — including
JavaScript-rendered, anti-bot protected and geo-restricted ones — with three
tools it can call from generateText and streamText.
npm install @zenrows/ai-sdk-toolsRequires ai v7 and zod as peer dependencies, and Node 22+.
import { generateText, isStepCount } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { createZenrowsTools } from '@zenrows/ai-sdk-tools';
const zenrows = createZenrowsTools({ apiKey: process.env.ZENROWS_API_KEY! });
const { text } = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'Read https://news.ycombinator.com and list the top three stories.',
tools: zenrows,
stopWhen: isStepCount(5),
});
console.log(text);Get an API key at app.zenrows.com.
Set it as ZENROWS_API_KEY, or pass it however your app handles secrets — the
key is only ever read by the factory, never by the model.
| Tool | What the model uses it for |
|---|---|
scrapeUrl |
Read a whole page as Markdown, plain text or HTML |
extractData |
Pull named fields out of a page as JSON, without reading all of it |
takeScreenshot |
Capture the page as an image a vision model can look at |
createZenrowsTools returns all three. Import them individually if you only
want one:
import { scrapeUrl } from '@zenrows/ai-sdk-tools';
const result = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'Summarise https://example.com',
tools: { scrapeUrl: scrapeUrl({ apiKey: process.env.ZENROWS_API_KEY! }) },
stopWhen: isStepCount(3),
});// app/api/chat/route.ts
import { isStepCount, streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { createZenrowsTools } from '@zenrows/ai-sdk-tools';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic('claude-sonnet-4-5'),
system: 'You are a research assistant. When asked about a URL, read it before answering.',
messages,
tools: createZenrowsTools({ apiKey: process.env.ZENROWS_API_KEY! }),
stopWhen: isStepCount(10),
});
return result.toUIMessageStreamResponse();
}Every option is set once, on the factory. The model never sees them.
| Option | Default | What it does |
|---|---|---|
apiKey |
— | Required. Your Zenrows API key. |
adaptiveStealth |
true |
Zenrows starts with the cheapest request that works and escalates only when the target needs it. |
defaultResponseType |
'markdown' |
Format scrapeUrl returns when the model does not ask for one. |
maxContentLength |
100_000 |
Characters of page content handed to the model before truncation. |
concurrency |
5 |
Concurrent requests to Zenrows. |
retries |
0 |
Retries on 422/503/504 and network errors, except Extract's 422 no_data_extracted, which is a final answer. |
Zenrows can decide per request whether a page needs JavaScript rendering or a
premium proxy, and bills for the configuration that worked. That is on by
default here, which is why the model is not given a jsRender or
premiumProxy knob to guess at.
Zenrows rejects adaptive stealth combined with an explicit tier flag, so an option that needs a browser — a screenshot, a wait-for-selector, a country proxy — drops it for that one call and sets what the option actually needs. The model cannot produce a request Zenrows will refuse.
Turn it off with adaptiveStealth: false if you would rather pay a flat,
predictable cost per request and accept that protected pages will fail.
A scraped page goes into the model's context and you pay for every character
of it. HTML routinely runs past 80,000 characters for an ordinary page, so
scrapeUrl truncates at maxContentLength and sets truncated: true rather
than quietly sending a large bill. Prefer extractData when you know which
fields you want.
scrapeUrl returns { content, truncated, url, finalUrl, creditsSpent, costUsd }.
extractData returns { data, url, creditsSpent, costUsd }. Both return
{ error } instead when the call fails, with a message the model can act on —
they do not throw, so a failed scrape costs a step rather than the run.
takeScreenshot returns the image to your code as base64 in
{ image, mediaType, bytes }, and hands the model a real image part via
toModelOutput, so a vision-capable model actually sees the page rather than a
base64 string.
Zenrows bills per request. Adaptive stealth means you pay for the configuration
that succeeded rather than for the most expensive one up front. Every tool
result carries creditsSpent, the credits the request used (from the
X-Request-Credits header), and costUsd, its cost in US dollars (from
X-Request-Cost), so the model can report what a piece of research cost.
When Extract recognises a page but finds no data on it, Zenrows answers 422
no_data_extracted and does not charge the request. extractData returns that
as an error telling the model to read the page with scrapeUrl instead of
repeating the call. A domain Extract has not been prepared for answers 403
REQS007, also not charged.
pnpm check:live runs every tool against Zenrows directly, without a model or
an LLM provider key — it only needs ZENROWS_API_KEY. pnpm example runs the
same tools through a real generateText tool-call loop and additionally needs
ANTHROPIC_API_KEY.
MIT