-
Notifications
You must be signed in to change notification settings - Fork 23
Add Tavily Tool-calls #541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
export const CREDIT_PRICE = 0.008; // $0.008 per credit | ||
|
||
// Tavily Search pricing | ||
export const TAVILY_SEARCH_PRICING = { | ||
basic: 1, // 1 credit per request | ||
advanced: 2, // 2 credits per request | ||
} as const; | ||
|
||
// Tavily Extract pricing | ||
export const TAVILY_EXTRACT_PRICING = { | ||
basic: { | ||
creditsPerUnit: 1, | ||
urlsPerCredit: 5, // Every 5 successful URL extractions cost 1 credit | ||
}, | ||
advanced: { | ||
creditsPerUnit: 2, | ||
urlsPerCredit: 5, // Every 5 successful URL extractions cost 2 credits | ||
}, | ||
} as const; | ||
|
||
// Tavily Map pricing | ||
export const TAVILY_MAP_PRICING = { | ||
regular: { | ||
creditsPerUnit: 1, | ||
pagesPerCredit: 10, // Every 10 successful pages cost 1 credit | ||
}, | ||
withInstructions: { | ||
creditsPerUnit: 2, | ||
pagesPerCredit: 10, // Every 10 successful pages with instructions cost 2 credits | ||
}, | ||
} as const; | ||
|
||
// Calculate costs | ||
export function calculateSearchCost( | ||
searchDepth: 'basic' | 'advanced' = 'basic' | ||
): number { | ||
return TAVILY_SEARCH_PRICING[searchDepth] * CREDIT_PRICE; | ||
} | ||
|
||
export function calculateExtractCost( | ||
successfulUrls: number, | ||
extractionDepth: 'basic' | 'advanced' = 'basic' | ||
): number { | ||
const { creditsPerUnit, urlsPerCredit } = | ||
TAVILY_EXTRACT_PRICING[extractionDepth]; | ||
const credits = Math.ceil(successfulUrls / urlsPerCredit) * creditsPerUnit; | ||
return credits * CREDIT_PRICE; | ||
} | ||
|
||
export function calculateMapCost( | ||
successfulPages: number, | ||
withInstructions: boolean = false | ||
): number { | ||
const pricing = withInstructions | ||
? TAVILY_MAP_PRICING.withInstructions | ||
: TAVILY_MAP_PRICING.regular; | ||
const credits = | ||
Math.ceil(successfulPages / pricing.pagesPerCredit) * | ||
pricing.creditsPerUnit; | ||
return credits * CREDIT_PRICE; | ||
} | ||
|
||
export function calculateCrawlCost( | ||
successfulPages: number, | ||
extractionDepth: 'basic' | 'advanced' = 'basic' | ||
): number { | ||
// Crawl cost = Mapping cost + Extraction cost | ||
const mappingCost = calculateMapCost(successfulPages, false); | ||
const extractionCost = calculateExtractCost(successfulPages, extractionDepth); | ||
return mappingCost + extractionCost; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { buildX402Response, isApiRequest, isX402Request } from 'utils'; | ||
import { TavilySearchInputSchema } from './types'; | ||
import { calculateTavilySearchCost, tavilySearch } from './tavily'; | ||
import { authenticateRequest } from 'auth'; | ||
import { prisma } from 'server'; | ||
import { settle } from 'handlers'; | ||
import { finalize } from 'handlers'; | ||
import { createTavilyTransaction } from './tavily'; | ||
import logger from 'logger'; | ||
import { Request, Response } from 'express'; | ||
|
||
export async function tavilySearchRoute(req: Request, res: Response) { | ||
try { | ||
const headers = req.headers as Record<string, string>; | ||
|
||
const inputBody = TavilySearchInputSchema.parse(req.body); | ||
|
||
const maxCost = calculateTavilySearchCost(inputBody); | ||
|
||
if (!isApiRequest(headers) && !isX402Request(headers)) { | ||
return buildX402Response(req, res, maxCost); | ||
} | ||
|
||
if (isApiRequest(headers)) { | ||
const { echoControlService } = await authenticateRequest(headers, prisma); | ||
|
||
const output = await tavilySearch(inputBody); | ||
|
||
const transaction = createTavilyTransaction(inputBody, output, maxCost); | ||
|
||
await echoControlService.createTransaction(transaction, maxCost); | ||
|
||
return res.status(200).json(output); | ||
} else if (isX402Request(headers)) { | ||
const settleResult = await settle(req, res, headers, maxCost); | ||
if (!settleResult) { | ||
return buildX402Response(req, res, maxCost); | ||
} | ||
const { payload, paymentAmountDecimal } = settleResult; | ||
|
||
const output = await tavilySearch(inputBody); | ||
|
||
const transaction = createTavilyTransaction(inputBody, output, maxCost); | ||
|
||
await finalize(paymentAmountDecimal, transaction, payload); | ||
|
||
return res.status(200).json(output); | ||
Comment on lines
+34
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for X402 payments - if the Tavily API call fails after payment settlement, users won't receive refunds. View Details📝 Patch Detailsdiff --git a/packages/app/server/src/resources/tavily/route.ts b/packages/app/server/src/resources/tavily/route.ts
index b8f9c685..cb322777 100644
--- a/packages/app/server/src/resources/tavily/route.ts
+++ b/packages/app/server/src/resources/tavily/route.ts
@@ -1,10 +1,10 @@
-import { buildX402Response, isApiRequest, isX402Request } from 'utils';
+import { buildX402Response, isApiRequest, isX402Request, decimalToUsdcBigInt } from 'utils';
import { TavilySearchInputSchema } from './types';
import { calculateTavilySearchCost, tavilySearch } from './tavily';
import { authenticateRequest } from 'auth';
import { prisma } from 'server';
-import { settle } from 'handlers';
-import { finalize } from 'handlers';
+import { settle, finalize } from 'handlers';
+import { transfer } from 'transferWithAuth';
import { createTavilyTransaction } from './tavily';
import logger from 'logger';
import { Request, Response } from 'express';
@@ -38,13 +38,21 @@ export async function tavilySearchRoute(req: Request, res: Response) {
}
const { payload, paymentAmountDecimal } = settleResult;
- const output = await tavilySearch(inputBody);
+ try {
+ const output = await tavilySearch(inputBody);
- const transaction = createTavilyTransaction(inputBody, output, maxCost);
+ const transaction = createTavilyTransaction(inputBody, output, maxCost);
- await finalize(paymentAmountDecimal, transaction, payload);
+ await finalize(paymentAmountDecimal, transaction, payload);
- return res.status(200).json(output);
+ return res.status(200).json(output);
+ } catch (error) {
+ // Full refund on error, like the main handleX402Request handler does
+ const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal);
+ const authPayload = payload.authorization;
+ await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt);
+ throw error;
+ }
} else {
return buildX402Response(req, res, maxCost);
}
AnalysisMissing error handling in tavily X402 route causes payment loss on API failuresWhat fails: How to reproduce: # Send X402 payment request to tavily endpoint with invalid/expired API key
curl -X POST /tavily/search \
-H "Content-Type: application/json" \
-H "X-Payment: <valid_payment_payload>" \
-d '{"query": "test", "search_depth": "basic"}' Result: Payment gets settled successfully, then Expected: Should perform full refund like Root cause: Tavily route calls |
||
} else { | ||
return buildX402Response(req, res, maxCost); | ||
} | ||
} catch (error) { | ||
logger.error('Error searching tavily', error); | ||
return res.status(500).json({ error: 'Internal server error' }); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i dont get this change. we are no longer returning the 402 response?