Skip to main content

Proxy Tavily AI Search via CloudBase Cloud Function

In one sentence: Use @tavily/core in a CloudBase Cloud Function to call the Tavily Search API, get LLM-friendly search results with answer / 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

DependencyVersion
@tavily/corelatest (run npm view @tavily/core version to check)
Node.js (Cloud Function runtime)≥ 18
@cloudbase/clilatest
Cloud Function typeWeb Cloud Function (HTTP trigger) — the frontend calls it directly via fetch
Public network egressCloud 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 name tavily-js. Installing the wrong package is the most common first mistake.

Step 1: Get a Tavily Key and Set Environment Variables in CloudBase

  1. Register at tavily.com, then go to "API Keys" in the dashboard and generate a key. It looks like tvly-xxxxxxxxxxxxxxxxxxxx.
  2. Generate a proxy access token locally:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  1. 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 generated
    • PROXY_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 rawContent is 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:

  1. Under "Environment Variables", add TAVILY_API_KEY and PROXY_ACCESS_TOKEN (the values from Step 1).
  2. 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.

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 (use extractDepth: "advanced" for more thorough parsing)
  • client.crawl("base-url", { maxDepth, maxBreadth }) — crawl an entire site and return content from multiple pages
  • client.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

SymptomCauseFix
Cannot find module 'tavily-js'Wrong package name installedUninstall and run npm i @tavily/core; tavily-js is the GitHub repo name, not the npm package name
401 Unauthorized from TavilyTAVILY_API_KEY not set, incorrect, or revokedCheck the Console "Environment Variables"; verify the key is still active in the Tavily Dashboard
429 Too Many RequestsConcurrent or monthly call limit exceeded for the current planReduce concurrency / upgrade plan / add throttling in the Cloud Function; check exact limits in the Tavily Console
Invalid value for parameter searchDepthPassed 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 publishedDateSome sites do not publish a date fieldThis 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.comCloud Function has no public network egressGo to Console → "Network Configuration" and enable public access; VPC environments need a NAT gateway
502 tavily_error occurs occasionally, retrying succeedsUpstream network instability or a brief Tavily 5xxImplement at most 2 exponential-backoff retries on the caller side; do not retry indefinitely

Full error code reference: https://docs.cloudbase.net/error-code/

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.