Proxy Tavily AI Search via CloudBase Cloud Function
In one sentence: Use
@tavily/corein a CloudBase Cloud Function to call the Tavily Search API, get LLM-friendly search results withanswer/results[]/score, expose them over HTTP, and keep the Tavily key server-side.Estimated time: 25 minutes | Difficulty: Advanced
Applicable Scenarios
- Applicable: adding real-time web retrieval to an LLM agent and feeding results directly into the model for search-augmented generation
- Applicable: keeping the Tavily API key out of the frontend / Mini Program by routing through a backend proxy
- Applicable: complementing add-rag-with-pgvector-cloudbase — RAG answers private-domain questions, Tavily answers questions about public web content
- Not applicable: pure private-domain knowledge retrieval (RAG + a vector store is more appropriate; Tavily does not crawl your intranet)
- Not applicable: asking the LLM about content already in its parametric memory (e.g., explaining a classic algorithm) — unnecessary overhead
Prerequisites
| Dependency | Version |
|---|---|
@tavily/core | latest (run npm view @tavily/core version to check) |
| Node.js (Cloud Function runtime) | ≥ 18 |
@cloudbase/cli | latest |
| Cloud Function type | Web Cloud Function (HTTP trigger) — the frontend calls it directly via fetch |
| Public network egress | Cloud Functions can reach the public internet by default; environments with a custom VPC must verify that a NAT gateway is attached |
You will need:
- A Tavily API key (register at tavily.com; the free tier is enough to get started — pricing beyond the free quota is per-call, see the official pricing page)
- A token for authenticating frontend callers (the simplest option is a random 32-byte string, referred to below as
PROXY_ACCESS_TOKEN)
Package name note: the official Tavily JavaScript SDK is published on npm as
@tavily/core, not as the GitHub repository nametavily-js. Installing the wrong package is the most common first mistake.
Step 1: Get a Tavily Key and Set Environment Variables in CloudBase
- Register at tavily.com, then go to "API Keys" in the dashboard and generate a key. It looks like
tvly-xxxxxxxxxxxxxxxxxxxx. - Generate a proxy access token locally:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
- Keep both values handy. After deploying the function, add them under "Cloud Functions → Environment Variables" in the Console:
TAVILY_API_KEY= the Tavily key you just generatedPROXY_ACCESS_TOKEN= the random string you just generated
Step 2: Write the Cloud Function (Full Search + Parameter Validation + Response Trimming)
mkdir tavily-search && cd tavily-search
npm init -y
npm install --save express @tavily/core
Set main to index.js in package.json and add a start script:
{
"name": "tavily-search",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2",
"@tavily/core": "^0.5.0"
}
}
index.js:
const express = require('express');
const { tavily } = require('@tavily/core');
const app = express();
app.use(express.json({ limit: '1mb' }));
const TAVILY_API_KEY = process.env.TAVILY_API_KEY;
const PROXY_ACCESS_TOKEN = process.env.PROXY_ACCESS_TOKEN;
if (!TAVILY_API_KEY || !PROXY_ACCESS_TOKEN) {
console.error('Missing TAVILY_API_KEY or PROXY_ACCESS_TOKEN env');
process.exit(1);
}
const client = tavily({ apiKey: TAVILY_API_KEY });
// Auth: validate Authorization: Bearer <PROXY_ACCESS_TOKEN>
function requireAuth(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
if (token !== PROXY_ACCESS_TOKEN) {
return res.status(401).json({ error: 'unauthorized' });
}
next();
}
// Allowlist of search parameters the frontend may pass through.
// Anything not on this list is dropped to prevent cost amplification.
const ALLOWED_OPTIONS = new Set([
'searchDepth', // "basic" | "advanced"
'topic', // "general" | "news" | "finance"
'timeRange', // "day"|"week"|"month"|"year"|"d"|"w"|"m"|"y"
'startDate', // YYYY-MM-DD
'endDate',
'maxResults', // 0-20
'chunksPerSource', // only effective with advanced search
'includeImages',
'includeAnswer', // false | "basic" | "advanced"
'includeRawContent', // false | "markdown" | "text"
'includeDomains', // max 300
'excludeDomains', // max 150
'country',
'timeout', // seconds
'exactMatch',
'includeFavicon',
]);
function pickOptions(input = {}) {
const out = {};
for (const [k, v] of Object.entries(input)) {
if (ALLOWED_OPTIONS.has(k)) out[k] = v;
}
// Enforce a safe default to prevent the frontend from requesting too many results
if (typeof out.maxResults !== 'number' || out.maxResults < 1) out.maxResults = 5;
if (out.maxResults > 10) out.maxResults = 10;
return out;
}
app.post('/search', requireAuth, async (req, res) => {
const { query, options } = req.body || {};
if (typeof query !== 'string' || !query.trim()) {
return res.status(400).json({ error: 'invalid_query', message: 'body.query must be a non-empty string' });
}
const safeOptions = pickOptions(options);
let response;
try {
response = await client.search(query, safeOptions);
} catch (err) {
console.error('tavily search failed', err);
// Tavily SDK errors are typically HTTP errors — pass the status code through for easier frontend debugging
const status = err.status || err.response?.status || 502;
return res.status(status).json({
error: 'tavily_error',
message: err.message || 'tavily request failed',
});
}
// Trim the response: LLMs generally only need query / answer / results.
// rawContent can be hundreds of KB; omit it by default. Frontends that need it can enable includeRawContent explicitly.
const slim = {
query: response.query,
answer: response.answer,
results: (response.results || []).map((r) => ({
title: r.title,
url: r.url,
content: r.content,
score: r.score,
publishedDate: r.publishedDate,
})),
responseTime: response.responseTime,
requestId: response.requestId,
};
res.json(slim);
});
app.get('/health', (_req, res) => res.json({ ok: true }));
const PORT = process.env.PORT || 9000;
app.listen(PORT, () => {
console.log(`tavily-search listening on ${PORT}`);
});
A few things worth noting:
searchDepth: "basic"returns results sufficient for most LLM use cases — fast and cheaper per call.searchDepth: "advanced"performs multiple fetches and splits pages into chunks for higher quality, but latency and cost both increase. Choose based on the scenario; do not hard-code this on the function side.includeAnswer: "basic"asks Tavily to return a one-sentence summary, saving a round-trip LLM call for summarization. Use"advanced"for a longer summary.- The parameter allowlist matters: some Tavily options (e.g.,
includeImages/includeRawContent) significantly expand the response body and cost. The frontend should not be able to enable these freely. - Trimming
rawContentis the highest-value optimization — raw HTML across multiple results can reach hundreds of KB, which degrades browser performance and wastes token budget.
Step 3: Deploy to CloudBase
tcb login
tcb fn deploy tavily-search --httpFn -e your-env-id
After deployment, go to "Cloud Functions → tavily-search" in the Console and do two things:
- Under "Environment Variables", add
TAVILY_API_KEYandPROXY_ACCESS_TOKEN(the values from Step 1). - Under "Trigger", confirm the HTTP Access Service is enabled and note the access URL, e.g.
https://your-env.service.tcloudbase.com/tavily-search.
Environment variable changes take effect on the next cold start. To apply immediately, trigger a redeployment in the Console, or change any minor setting (e.g., memory or timeout) and save — this restarts the instance.
Step 4: Call from Frontend / Agent and Feed Results to the LLM
A minimal fetch call:
const resp = await fetch('https://your-env.service.tcloudbase.com/tavily-search/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_PROXY_ACCESS_TOKEN',
},
body: JSON.stringify({
query: 'Fed rate decision April 2026',
options: {
searchDepth: 'basic',
topic: 'news',
timeRange: 'week',
maxResults: 5,
includeAnswer: 'basic',
},
}),
});
const data = await resp.json();
console.log(data.answer);
console.log(data.results.map((r) => `${r.title} — ${r.url}`));
When injecting results into an LLM context, format results as numbered source citations so the model can reference them in its answer:
const sources = data.results
.map((r, i) => `[${i + 1}] ${r.title}\n${r.content}\nSource: ${r.url}`)
.join('\n\n');
const userPrompt = `
Answer the user's question based on the following search results. Cite sources using [1] [2] notation:
${sources}
User question: What was the outcome of the Fed's April 2026 rate decision?
`;
To wire this into a complete search-augmented chatbot, you also need an LLM proxy — see connect-openai-api-cloud-function.
Verification
curl -X POST 'https://your-env.service.tcloudbase.com/tavily-search/search' \
-H 'Authorization: Bearer YOUR_PROXY_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"query": "What is CloudBase",
"options": { "searchDepth": "basic", "maxResults": 3, "includeAnswer": "basic" }
}'
Expected response structure:
{
"query": "What is CloudBase",
"answer": "CloudBase is a cloud development platform by Tencent Cloud...",
"results": [
{ "title": "...", "url": "https://...", "content": "...", "score": 0.91, "publishedDate": null }
],
"responseTime": 1.09,
"requestId": "uuid"
}
responseTime is typically 0.5–3 seconds. With searchDepth: "advanced" it can reach 5–15 seconds — set the function timeout to 30 seconds or more.
Tavily Is More Than Search
The same client object from @tavily/core exposes several additional methods with a similar API:
client.extract(["url1", "url2"], { ... })— extract the main content of specific URLs (useextractDepth: "advanced"for more thorough parsing)client.crawl("base-url", { maxDepth, maxBreadth })— crawl an entire site and return content from multiple pagesclient.map(...)— generate a site URL map without fetching page content
To add any of these capabilities, add a new route to the Cloud Function and reuse the same client instance — no need to call tavily() again.
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
Cannot find module 'tavily-js' | Wrong package name installed | Uninstall and run npm i @tavily/core; tavily-js is the GitHub repo name, not the npm package name |
401 Unauthorized from Tavily | TAVILY_API_KEY not set, incorrect, or revoked | Check the Console "Environment Variables"; verify the key is still active in the Tavily Dashboard |
429 Too Many Requests | Concurrent or monthly call limit exceeded for the current plan | Reduce concurrency / upgrade plan / add throttling in the Cloud Function; check exact limits in the Tavily Console |
Invalid value for parameter searchDepth | Passed an unrecognized value like "deep" or "full" | searchDepth only accepts "basic" or "advanced"; older Python SDK used snake_case search_depth — the JS SDK uses camelCase searchDepth |
topic: "news" results have no publishedDate | Some sites do not publish a date field | This is not a bug; add a null check in the frontend. To strictly require a date, filter results to only those where r.publishedDate is set and instruct the model accordingly |
getaddrinfo ENOTFOUND api.tavily.com | Cloud Function has no public network egress | Go to Console → "Network Configuration" and enable public access; VPC environments need a NAT gateway |
502 tavily_error occurs occasionally, retrying succeeds | Upstream network instability or a brief Tavily 5xx | Implement at most 2 exponential-backoff retries on the caller side; do not retry indefinitely |
Full error code reference: https://docs.cloudbase.net/error-code/
Related Documentation
- connect-openai-api-cloud-function — Proxy an LLM API via Cloud Function; combine with this recipe for a complete search-augmented chatbot
- add-rag-with-pgvector-cloudbase — Private-domain knowledge RAG, complementary to Tavily
- secure-secrets-in-cloud-function — Layered management of sensitive values like
TAVILY_API_KEYacross local / CI / production - HTTP Cloud Functions — Quick start for Web Cloud Functions (HTTP trigger)
- Function Environment Variables — Configuring and reading injected variables
Next Steps
Once the search proxy is running, the next step is connecting it to an LLM to form a complete search-augmented pipeline: the frontend calls tavily-search, appends results[] to the messages array, and sends the request to the LLM proxy built in connect-openai-api-cloud-function — the model then answers based on the retrieved results. To add a Perplexity-style "search-while-answering" SSE streaming experience, wrap the pipeline with add-vercel-ai-sdk-streaming-chatbot.