From e30228f6153ee200206138b474762c3053cf0615 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Thu, 9 Oct 2025 12:11:19 -0400 Subject: [PATCH 1/4] add-resource-tavily --- packages/app/server/src/handlers.ts | 217 +++++++++--------- .../app/server/src/resources/tavily/prices.ts | 66 ++++++ .../app/server/src/resources/tavily/tavily.ts | 32 +++ .../app/server/src/resources/tavily/types.ts | 52 +++++ packages/app/server/src/routers/resource.ts | 93 ++++++++ 5 files changed, 356 insertions(+), 104 deletions(-) create mode 100644 packages/app/server/src/resources/tavily/prices.ts create mode 100644 packages/app/server/src/resources/tavily/tavily.ts create mode 100644 packages/app/server/src/resources/tavily/types.ts create mode 100644 packages/app/server/src/routers/resource.ts diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index b63182899..c906a661a 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -23,34 +23,30 @@ import { } from 'services/facilitator/x402-types'; import { Decimal } from '@prisma/client/runtime/library'; import logger from 'logger'; - -export async function handleX402Request({ - req, - res, - headers, - maxCost, - isPassthroughProxyRoute, - provider, - isStream, -}: X402HandlerInput) { - if (isPassthroughProxyRoute) { - return await makeProxyPassthroughRequest(req, res, provider, headers); - } - - // Apply x402 payment middleware with the calculated maxCost +import { Request, Response } from 'express'; + +export async function settle( + req: Request, + res: Response, + headers: Record, + maxCost: Decimal +): Promise<{ payload: ExactEvmPayload; paymentAmountDecimal: Decimal } | undefined> { const network = process.env.NETWORK as Network; let recipient: string; try { recipient = (await getSmartAccount()).smartAccount.address; } catch (error) { - return buildX402Response(req, res, maxCost); + buildX402Response(req, res, maxCost); + return undefined; } + let xPaymentData: PaymentPayload; try { xPaymentData = validateXPaymentHeader(headers, req); } catch (error) { - return buildX402Response(req, res, maxCost); + buildX402Response(req, res, maxCost); + return undefined; } const payload = xPaymentData.payload as ExactEvmPayload; @@ -62,99 +58,112 @@ export async function handleX402Request({ // Note(shafu, alvaro): Edge case where client sends the x402-challenge // but the payment amount is less than what we returned in the first response if (BigInt(paymentAmount) < decimalToUsdcBigInt(maxCost)) { - return buildX402Response(req, res, maxCost); + buildX402Response(req, res, maxCost); + return undefined; } const facilitatorClient = new FacilitatorClient(); - try { - // Default to no refund - let refundAmount = new Decimal(0); - let transaction: Transaction | null = null; - let data: unknown = null; - - // Construct and validate PaymentRequirements using Zod schema - const paymentRequirements = PaymentRequirementsSchema.parse({ - scheme: 'exact', - network, - maxAmountRequired: paymentAmount, - resource: `${req.protocol}://${req.get('host')}${req.url}`, - description: 'Echo x402', - mimeType: 'application/json', - payTo: recipient, - maxTimeoutSeconds: 60, - asset: USDC_ADDRESS, - extra: { - name: 'USD Coin', - version: '2', - }, - }); - // Validate and execute settle request - const settleRequest = SettleRequestSchema.parse({ - paymentPayload: xPaymentData, - paymentRequirements, + const paymentRequirements = PaymentRequirementsSchema.parse({ + scheme: 'exact', + network, + maxAmountRequired: paymentAmount, + resource: `${req.protocol}://${req.get('host')}${req.url}`, + description: 'Echo x402', + mimeType: 'application/json', + payTo: recipient, + maxTimeoutSeconds: 60, + asset: USDC_ADDRESS, + extra: { + name: 'USD Coin', + version: '2', + }, + }); + + const settleRequest = SettleRequestSchema.parse({ + paymentPayload: xPaymentData, + paymentRequirements, + }); + + const settleResult = await facilitatorClient.settle(settleRequest); + + if (!settleResult.success || !settleResult.transaction) { + buildX402Response(req, res, maxCost); + return undefined; + } + + return { payload, paymentAmountDecimal }; +} + +export async function finalize( + paymentAmountDecimal: Decimal, + transaction: Transaction, + payload: ExactEvmPayload +) { + const refundAmount = calculateRefundAmount( + paymentAmountDecimal, + transaction.rawTransactionCost + ); + + if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { + const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); + const authPayload = payload.authorization; + await transfer( + authPayload.from as `0x${string}`, + refundAmountUsdcBigInt + ).catch(transferError => { + logger.error('Failed to process refund', { + error: transferError, + refundAmount: refundAmount.toString(), + }); }); + } +} + +export async function handleX402Request({ + req, + res, + headers, + maxCost, + isPassthroughProxyRoute, + provider, + isStream, +}: X402HandlerInput) { + if (isPassthroughProxyRoute) { + return await makeProxyPassthroughRequest(req, res, provider, headers); + } + + const settleResult = await settle(req, res, headers, maxCost); + if (!settleResult) { + return; + } - const settleResult = await facilitatorClient.settle(settleRequest); - - if (!settleResult.success || !settleResult.transaction) { - return buildX402Response(req, res, maxCost); - } - - try { - const transactionResult = await modelRequestService.executeModelRequest( - req, - res, - headers, - provider, - isStream - ); - transaction = transactionResult.transaction; - data = transactionResult.data; - - // Send the response - the middleware has intercepted res.end()/res.json() - // and will actually send it after settlement completes - modelRequestService.handleResolveResponse(res, isStream, data); - - refundAmount = calculateRefundAmount( - paymentAmountDecimal, - transaction.rawTransactionCost - ); - - // Process refund if needed - if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { - const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); - const authPayload = payload.authorization; - await transfer( - authPayload.from as `0x${string}`, - refundAmountUsdcBigInt - ).catch(transferError => { - logger.error('Failed to process refund', { - error: transferError, - refundAmount: refundAmount.toString(), - }); - }); - } - } catch (error) { - // In case of error, do full refund - refundAmount = paymentAmountDecimal; - - if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { - const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); - const authPayload = payload.authorization; - await transfer( - authPayload.from as `0x${string}`, - refundAmountUsdcBigInt - ).catch(transferError => { - logger.error('Failed to process full refund after error', { - error: transferError, - originalError: error, - refundAmount: refundAmount.toString(), - }); - }); - } - } + const { payload, paymentAmountDecimal } = settleResult; + + try { + const transactionResult = await modelRequestService.executeModelRequest( + req, + res, + headers, + provider, + isStream + ); + + modelRequestService.handleResolveResponse(res, isStream, transactionResult.data); + + await finalize(paymentAmountDecimal, transactionResult.transaction, payload); } catch (error) { - throw error; + const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); + const authPayload = payload.authorization; + await transfer( + authPayload.from as `0x${string}`, + refundAmountUsdcBigInt + ).catch(transferError => { + logger.error('Failed to process full refund after error', { + error: transferError, + originalError: error, + refundAmount: paymentAmountDecimal.toString(), + }); + }); } } diff --git a/packages/app/server/src/resources/tavily/prices.ts b/packages/app/server/src/resources/tavily/prices.ts new file mode 100644 index 000000000..af8982dc3 --- /dev/null +++ b/packages/app/server/src/resources/tavily/prices.ts @@ -0,0 +1,66 @@ +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; +} \ No newline at end of file diff --git a/packages/app/server/src/resources/tavily/tavily.ts b/packages/app/server/src/resources/tavily/tavily.ts new file mode 100644 index 000000000..76486c093 --- /dev/null +++ b/packages/app/server/src/resources/tavily/tavily.ts @@ -0,0 +1,32 @@ +import { Decimal } from "@prisma/client/runtime/library"; +import { CREDIT_PRICE, TAVILY_SEARCH_PRICING } from "./prices"; +import { TavilySearchInput, TavilySearchOutput, TavilySearchOutputSchema } from "./types"; + +export const calculateTavilySearchCost = (input: TavilySearchInput): Decimal => { + const price = TAVILY_SEARCH_PRICING[input.search_depth ?? "basic"]; + return new Decimal(price).mul(CREDIT_PRICE); +} +const TAVILY_API_KEY = process.env.TAVILY_API_KEY; +export async function tavilySearch( + input: TavilySearchInput, +): Promise { + const response = await fetch("https://api.tavily.com/search", { + method: "POST", + headers: { + Authorization: `Bearer ${TAVILY_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Tavily API request failed: ${response.status} ${response.statusText} - ${errorText}` + ); + } + + const data = await response.json(); + return TavilySearchOutputSchema.parse(data); +} + diff --git a/packages/app/server/src/resources/tavily/types.ts b/packages/app/server/src/resources/tavily/types.ts new file mode 100644 index 000000000..6f56782b1 --- /dev/null +++ b/packages/app/server/src/resources/tavily/types.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +// Input schema +export const TavilySearchInputSchema = z.object({ + query: z.string(), + auto_parameters: z.boolean().optional(), + topic: z.enum(["general", "news"]).optional(), + search_depth: z.enum(["basic", "advanced"]).optional(), + chunks_per_source: z.number().int().positive().optional(), + max_results: z.number().int().positive().optional(), + time_range: z.string().nullable().optional(), + days: z.number().int().positive().optional(), + start_date: z.string().optional(), + end_date: z.string().optional(), + include_answer: z.boolean().optional(), + include_raw_content: z.boolean().optional(), + include_images: z.boolean().optional(), + include_image_descriptions: z.boolean().optional(), + include_favicon: z.boolean().optional(), + include_domains: z.array(z.string()).optional(), + exclude_domains: z.array(z.string()).optional(), + country: z.string().nullable().optional(), + }); + + export type TavilySearchInput = z.infer; + + // Output schema + export const TavilySearchResultSchema = z.object({ + title: z.string(), + url: z.string(), + content: z.string(), + score: z.number(), + raw_content: z.string().nullable(), + favicon: z.string().optional(), + }); + + export const TavilySearchOutputSchema = z.object({ + query: z.string(), + answer: z.string().optional(), + images: z.array(z.string()), + results: z.array(TavilySearchResultSchema), + auto_parameters: z + .object({ + topic: z.string().optional(), + search_depth: z.string().optional(), + }) + .optional(), + response_time: z.string(), + request_id: z.string(), + }); + + export type TavilySearchOutput = z.infer; \ No newline at end of file diff --git a/packages/app/server/src/routers/resource.ts b/packages/app/server/src/routers/resource.ts new file mode 100644 index 000000000..47ba481ba --- /dev/null +++ b/packages/app/server/src/routers/resource.ts @@ -0,0 +1,93 @@ +import express, { Request, Response, Router } from 'express'; +import path from 'path'; +import logger, { logMetric } from '../logger'; +import { TavilySearchInputSchema } from '../resources/tavily/types'; +import { calculateTavilySearchCost, tavilySearch } from '../resources/tavily/tavily'; +import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { authenticateRequest } from 'auth'; +import { prisma } from 'server'; +import { Transaction } from 'types'; +import { settle, finalize } from 'handlers'; +const resourceRouter: Router = Router(); + +resourceRouter.post('/tavily/search', async (req: Request, res: Response) => { + try { + const headers = req.headers as Record; + + const inputBody = TavilySearchInputSchema.parse(req.body); + + const maxCost = calculateTavilySearchCost(inputBody); + + if ( + !isApiRequest(headers) && + !isX402Request(headers) + ) { + buildX402Response(req, res, maxCost); + return; + } + + if (isApiRequest(headers)) { + const { echoControlService } = + await authenticateRequest(headers, prisma); + + const output = await tavilySearch(inputBody); + + const transaction: Transaction = { + metadata: { + providerId: output.request_id, + provider: 'tavily', + model: inputBody.search_depth ?? 'basic', + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + toolCost: maxCost, + }, + rawTransactionCost: maxCost, + status: 'completed', + }; + + 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) { + buildX402Response(req, res, maxCost); + return; + } + const { payload, paymentAmountDecimal } = settleResult; + + const output = await tavilySearch(inputBody); + + const transaction: Transaction = { + metadata: { + providerId: output.request_id, + provider: 'tavily', + model: inputBody.search_depth ?? 'basic', + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + toolCost: maxCost, + }, + rawTransactionCost: maxCost, + status: 'completed', + }; + + await finalize(paymentAmountDecimal, transaction, payload); + + return res.status(200).json(output); + } else { + buildX402Response(req, res, maxCost); + return; + } + } catch (error) { + logger.error('Error searching tavily', error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + + + + +export default resourceRouter; \ No newline at end of file From e53edfeb9e9a67cfaf98cbeab360bbb232efdd3d Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Thu, 9 Oct 2025 16:07:02 -0400 Subject: [PATCH 2/4] add tavily resource --- packages/app/server/src/resources/tavily/types.ts | 6 +++--- packages/app/server/src/schema/schemaForRoute.ts | 14 ++++++++++++++ packages/app/server/src/server.ts | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/app/server/src/resources/tavily/types.ts b/packages/app/server/src/resources/tavily/types.ts index 6f56782b1..f2f36a1ac 100644 --- a/packages/app/server/src/resources/tavily/types.ts +++ b/packages/app/server/src/resources/tavily/types.ts @@ -41,11 +41,11 @@ export const TavilySearchInputSchema = z.object({ results: z.array(TavilySearchResultSchema), auto_parameters: z .object({ - topic: z.string().optional(), - search_depth: z.string().optional(), + topic: z.any().optional(), + search_depth: z.any().optional(), }) .optional(), - response_time: z.string(), + response_time: z.number(), request_id: z.string(), }); diff --git a/packages/app/server/src/schema/schemaForRoute.ts b/packages/app/server/src/schema/schemaForRoute.ts index 429fedc4d..124c38803 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -3,6 +3,7 @@ import { GeminiFlashImageInputSchema, GeminiFlashImageOutputSchema } from "./ima import { z } from "zod"; import { ChatCompletionInput, ChatCompletionOutput } from "./chat/completions"; import { CreateImagesRequest, CreateImagesResponse } from "./image/openai"; +import { TavilySearchInputSchema, TavilySearchOutputSchema } from "../resources/tavily/types"; export function getSchemaForRoute(path: string): { input: { type: "http"; method: string; bodyFields?: unknown }; output: unknown } | undefined { if (path.endsWith("/videos")) { @@ -53,5 +54,18 @@ export function getSchemaForRoute(path: string): { input: { type: "http"; method output: outputSchema.properties, }; } + + if (path.endsWith("/tavily/search")) { + const inputSchema = z.toJSONSchema(TavilySearchInputSchema, { target: "openapi-3.0" }); + const outputSchema = z.toJSONSchema(TavilySearchOutputSchema, { target: "openapi-3.0" }); + return { + input: { + type: "http", + method: "POST", + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } return undefined; } \ No newline at end of file diff --git a/packages/app/server/src/server.ts b/packages/app/server/src/server.ts index b79f92b97..356b4cec3 100644 --- a/packages/app/server/src/server.ts +++ b/packages/app/server/src/server.ts @@ -19,6 +19,7 @@ import { handleX402Request, handleApiKeyRequest } from './handlers'; import { initializeProvider } from './services/ProviderInitializationService'; import { getRequestMaxCost } from './services/PricingService'; import { Decimal } from '@prisma/client/runtime/library'; +import resourceRouter from './routers/resource'; dotenv.config(); @@ -80,6 +81,9 @@ app.use(standardRouter); // Use in-flight monitor router for monitoring endpoints app.use(inFlightMonitorRouter); +// Use resource router for resource routes +app.use('/resource', resourceRouter); + // Main route handler app.all('*', async (req: EscrowRequest, res: Response, next: NextFunction) => { try { From 27dbd15b99970c7d278c5fcd396f24784f59432c Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Thu, 9 Oct 2025 18:08:54 -0400 Subject: [PATCH 3/4] cleanup --- .../app/server/src/resources/tavily/tavily.ts | 21 +++++++++++++ packages/app/server/src/routers/resource.ts | 31 ++----------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/packages/app/server/src/resources/tavily/tavily.ts b/packages/app/server/src/resources/tavily/tavily.ts index 76486c093..9dfc90808 100644 --- a/packages/app/server/src/resources/tavily/tavily.ts +++ b/packages/app/server/src/resources/tavily/tavily.ts @@ -1,11 +1,32 @@ import { Decimal } from "@prisma/client/runtime/library"; import { CREDIT_PRICE, TAVILY_SEARCH_PRICING } from "./prices"; import { TavilySearchInput, TavilySearchOutput, TavilySearchOutputSchema } from "./types"; +import { Transaction } from "types"; export const calculateTavilySearchCost = (input: TavilySearchInput): Decimal => { const price = TAVILY_SEARCH_PRICING[input.search_depth ?? "basic"]; return new Decimal(price).mul(CREDIT_PRICE); } + +export const createTavilyTransaction = ( + input: TavilySearchInput, + output: TavilySearchOutput, + cost: Decimal +): Transaction => { + return { + metadata: { + providerId: output.request_id, + provider: 'tavily', + model: input.search_depth ?? 'basic', + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + toolCost: cost, + }, + rawTransactionCost: cost, + status: 'completed', + }; +} const TAVILY_API_KEY = process.env.TAVILY_API_KEY; export async function tavilySearch( input: TavilySearchInput, diff --git a/packages/app/server/src/routers/resource.ts b/packages/app/server/src/routers/resource.ts index 47ba481ba..21877bd95 100644 --- a/packages/app/server/src/routers/resource.ts +++ b/packages/app/server/src/routers/resource.ts @@ -2,11 +2,10 @@ import express, { Request, Response, Router } from 'express'; import path from 'path'; import logger, { logMetric } from '../logger'; import { TavilySearchInputSchema } from '../resources/tavily/types'; -import { calculateTavilySearchCost, tavilySearch } from '../resources/tavily/tavily'; +import { calculateTavilySearchCost, tavilySearch, createTavilyTransaction } from '../resources/tavily/tavily'; import { buildX402Response, isApiRequest, isX402Request } from 'utils'; import { authenticateRequest } from 'auth'; import { prisma } from 'server'; -import { Transaction } from 'types'; import { settle, finalize } from 'handlers'; const resourceRouter: Router = Router(); @@ -32,19 +31,7 @@ resourceRouter.post('/tavily/search', async (req: Request, res: Response) => { const output = await tavilySearch(inputBody); - const transaction: Transaction = { - metadata: { - providerId: output.request_id, - provider: 'tavily', - model: inputBody.search_depth ?? 'basic', - inputTokens: 0, - outputTokens: 0, - totalTokens: 0, - toolCost: maxCost, - }, - rawTransactionCost: maxCost, - status: 'completed', - }; + const transaction = createTavilyTransaction(inputBody, output, maxCost); await echoControlService.createTransaction(transaction, maxCost); @@ -60,19 +47,7 @@ resourceRouter.post('/tavily/search', async (req: Request, res: Response) => { const output = await tavilySearch(inputBody); - const transaction: Transaction = { - metadata: { - providerId: output.request_id, - provider: 'tavily', - model: inputBody.search_depth ?? 'basic', - inputTokens: 0, - outputTokens: 0, - totalTokens: 0, - toolCost: maxCost, - }, - rawTransactionCost: maxCost, - status: 'completed', - }; + const transaction = createTavilyTransaction(inputBody, output, maxCost); await finalize(paymentAmountDecimal, transaction, payload); From 0cded6c0a3d3244595ad3297a9a3d3917241aa3f Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 19:36:24 -0400 Subject: [PATCH 4/4] format --- packages/app/server/src/handlers.ts | 37 ++--- .../app/server/src/resources/tavily/prices.ts | 17 +- .../app/server/src/resources/tavily/route.ts | 55 +++++++ .../app/server/src/resources/tavily/tavily.ts | 31 ++-- .../app/server/src/resources/tavily/types.ts | 98 +++++------ packages/app/server/src/routers/resource.ts | 65 ++------ .../app/server/src/schema/chat/completions.ts | 45 +++--- .../app/server/src/schema/image/gemini.ts | 152 +++++++++--------- .../app/server/src/schema/image/openai.ts | 6 +- .../app/server/src/schema/schemaForRoute.ts | 35 ++-- .../app/server/src/schema/video/openai.ts | 48 +++--- .../server/src/services/AccountingService.ts | 5 +- packages/sdk/aix402/README.md | 10 +- packages/sdk/aix402/package.json | 1 - packages/sdk/aix402/src/client.ts | 1 - packages/sdk/aix402/src/fetch-no-payment.ts | 65 ++++---- packages/sdk/aix402/src/fetch-with-payment.ts | 114 ++++++------- packages/sdk/aix402/src/server.ts | 5 +- packages/sdk/aix402/src/useChatWithPayment.ts | 46 +++--- packages/sdk/aix402/src/utils.ts | 10 +- packages/sdk/aix402/tsconfig.json | 1 - packages/sdk/aix402/tsup.config.ts | 1 - packages/sdk/aix402/vitest.config.ts | 1 - packages/sdk/echo-start/src/index.ts | 62 ++++--- .../sdk/examples/next-402-chat/package.json | 2 +- .../src/app/_components/chat-no-payment.tsx | 34 ++-- .../src/app/_components/chat.tsx | 28 ++-- .../src/app/_components/header.tsx | 5 +- .../src/app/api/chat-server-wallet/cdp.ts | 35 ++-- .../next-402-chat/src/app/api/chat/route.ts | 19 ++- .../src/components/wallet/config.ts | 13 +- .../examples/next-402-chat/src/lib/x402.ts | 10 +- 32 files changed, 565 insertions(+), 492 deletions(-) create mode 100644 packages/app/server/src/resources/tavily/route.ts diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index c906a661a..49d641e21 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -30,7 +30,9 @@ export async function settle( res: Response, headers: Record, maxCost: Decimal -): Promise<{ payload: ExactEvmPayload; paymentAmountDecimal: Decimal } | undefined> { +): Promise< + { payload: ExactEvmPayload; paymentAmountDecimal: Decimal } | undefined +> { const network = process.env.NETWORK as Network; let recipient: string; @@ -107,15 +109,7 @@ export async function finalize( if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); const authPayload = payload.authorization; - await transfer( - authPayload.from as `0x${string}`, - refundAmountUsdcBigInt - ).catch(transferError => { - logger.error('Failed to process refund', { - error: transferError, - refundAmount: refundAmount.toString(), - }); - }); + await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); } } @@ -148,22 +142,21 @@ export async function handleX402Request({ isStream ); - modelRequestService.handleResolveResponse(res, isStream, transactionResult.data); + modelRequestService.handleResolveResponse( + res, + isStream, + transactionResult.data + ); - await finalize(paymentAmountDecimal, transactionResult.transaction, payload); + await finalize( + paymentAmountDecimal, + transactionResult.transaction, + payload + ); } catch (error) { const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); const authPayload = payload.authorization; - await transfer( - authPayload.from as `0x${string}`, - refundAmountUsdcBigInt - ).catch(transferError => { - logger.error('Failed to process full refund after error', { - error: transferError, - originalError: error, - refundAmount: paymentAmountDecimal.toString(), - }); - }); + await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); } } diff --git a/packages/app/server/src/resources/tavily/prices.ts b/packages/app/server/src/resources/tavily/prices.ts index af8982dc3..ced0214e3 100644 --- a/packages/app/server/src/resources/tavily/prices.ts +++ b/packages/app/server/src/resources/tavily/prices.ts @@ -31,15 +31,18 @@ export const TAVILY_MAP_PRICING = { } as const; // Calculate costs -export function calculateSearchCost(searchDepth: "basic" | "advanced" = "basic"): number { +export function calculateSearchCost( + searchDepth: 'basic' | 'advanced' = 'basic' +): number { return TAVILY_SEARCH_PRICING[searchDepth] * CREDIT_PRICE; } export function calculateExtractCost( successfulUrls: number, - extractionDepth: "basic" | "advanced" = "basic" + extractionDepth: 'basic' | 'advanced' = 'basic' ): number { - const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[extractionDepth]; + const { creditsPerUnit, urlsPerCredit } = + TAVILY_EXTRACT_PRICING[extractionDepth]; const credits = Math.ceil(successfulUrls / urlsPerCredit) * creditsPerUnit; return credits * CREDIT_PRICE; } @@ -51,16 +54,18 @@ export function calculateMapCost( const pricing = withInstructions ? TAVILY_MAP_PRICING.withInstructions : TAVILY_MAP_PRICING.regular; - const credits = Math.ceil(successfulPages / pricing.pagesPerCredit) * pricing.creditsPerUnit; + const credits = + Math.ceil(successfulPages / pricing.pagesPerCredit) * + pricing.creditsPerUnit; return credits * CREDIT_PRICE; } export function calculateCrawlCost( successfulPages: number, - extractionDepth: "basic" | "advanced" = "basic" + extractionDepth: 'basic' | 'advanced' = 'basic' ): number { // Crawl cost = Mapping cost + Extraction cost const mappingCost = calculateMapCost(successfulPages, false); const extractionCost = calculateExtractCost(successfulPages, extractionDepth); return mappingCost + extractionCost; -} \ No newline at end of file +} diff --git a/packages/app/server/src/resources/tavily/route.ts b/packages/app/server/src/resources/tavily/route.ts new file mode 100644 index 000000000..b8f9c6852 --- /dev/null +++ b/packages/app/server/src/resources/tavily/route.ts @@ -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; + + 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); + } else { + return buildX402Response(req, res, maxCost); + } + } catch (error) { + logger.error('Error searching tavily', error); + return res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/packages/app/server/src/resources/tavily/tavily.ts b/packages/app/server/src/resources/tavily/tavily.ts index 9dfc90808..808797f72 100644 --- a/packages/app/server/src/resources/tavily/tavily.ts +++ b/packages/app/server/src/resources/tavily/tavily.ts @@ -1,12 +1,18 @@ -import { Decimal } from "@prisma/client/runtime/library"; -import { CREDIT_PRICE, TAVILY_SEARCH_PRICING } from "./prices"; -import { TavilySearchInput, TavilySearchOutput, TavilySearchOutputSchema } from "./types"; -import { Transaction } from "types"; +import { Decimal } from '@prisma/client/runtime/library'; +import { CREDIT_PRICE, TAVILY_SEARCH_PRICING } from './prices'; +import { + TavilySearchInput, + TavilySearchOutput, + TavilySearchOutputSchema, +} from './types'; +import { Transaction } from 'types'; -export const calculateTavilySearchCost = (input: TavilySearchInput): Decimal => { - const price = TAVILY_SEARCH_PRICING[input.search_depth ?? "basic"]; +export const calculateTavilySearchCost = ( + input: TavilySearchInput +): Decimal => { + const price = TAVILY_SEARCH_PRICING[input.search_depth ?? 'basic']; return new Decimal(price).mul(CREDIT_PRICE); -} +}; export const createTavilyTransaction = ( input: TavilySearchInput, @@ -26,16 +32,16 @@ export const createTavilyTransaction = ( rawTransactionCost: cost, status: 'completed', }; -} +}; const TAVILY_API_KEY = process.env.TAVILY_API_KEY; export async function tavilySearch( - input: TavilySearchInput, + input: TavilySearchInput ): Promise { - const response = await fetch("https://api.tavily.com/search", { - method: "POST", + const response = await fetch('https://api.tavily.com/search', { + method: 'POST', headers: { Authorization: `Bearer ${TAVILY_API_KEY}`, - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify(input), }); @@ -50,4 +56,3 @@ export async function tavilySearch( const data = await response.json(); return TavilySearchOutputSchema.parse(data); } - diff --git a/packages/app/server/src/resources/tavily/types.ts b/packages/app/server/src/resources/tavily/types.ts index f2f36a1ac..8f4deacf5 100644 --- a/packages/app/server/src/resources/tavily/types.ts +++ b/packages/app/server/src/resources/tavily/types.ts @@ -1,52 +1,52 @@ -import { z } from "zod"; +import { z } from 'zod'; // Input schema export const TavilySearchInputSchema = z.object({ - query: z.string(), - auto_parameters: z.boolean().optional(), - topic: z.enum(["general", "news"]).optional(), - search_depth: z.enum(["basic", "advanced"]).optional(), - chunks_per_source: z.number().int().positive().optional(), - max_results: z.number().int().positive().optional(), - time_range: z.string().nullable().optional(), - days: z.number().int().positive().optional(), - start_date: z.string().optional(), - end_date: z.string().optional(), - include_answer: z.boolean().optional(), - include_raw_content: z.boolean().optional(), - include_images: z.boolean().optional(), - include_image_descriptions: z.boolean().optional(), - include_favicon: z.boolean().optional(), - include_domains: z.array(z.string()).optional(), - exclude_domains: z.array(z.string()).optional(), - country: z.string().nullable().optional(), - }); - - export type TavilySearchInput = z.infer; - - // Output schema - export const TavilySearchResultSchema = z.object({ - title: z.string(), - url: z.string(), - content: z.string(), - score: z.number(), - raw_content: z.string().nullable(), - favicon: z.string().optional(), - }); - - export const TavilySearchOutputSchema = z.object({ - query: z.string(), - answer: z.string().optional(), - images: z.array(z.string()), - results: z.array(TavilySearchResultSchema), - auto_parameters: z - .object({ - topic: z.any().optional(), - search_depth: z.any().optional(), - }) - .optional(), - response_time: z.number(), - request_id: z.string(), - }); - - export type TavilySearchOutput = z.infer; \ No newline at end of file + query: z.string(), + auto_parameters: z.boolean().optional(), + topic: z.enum(['general', 'news']).optional(), + search_depth: z.enum(['basic', 'advanced']).optional(), + chunks_per_source: z.number().int().positive().optional(), + max_results: z.number().int().positive().optional(), + time_range: z.string().nullable().optional(), + days: z.number().int().positive().optional(), + start_date: z.string().optional(), + end_date: z.string().optional(), + include_answer: z.boolean().optional(), + include_raw_content: z.boolean().optional(), + include_images: z.boolean().optional(), + include_image_descriptions: z.boolean().optional(), + include_favicon: z.boolean().optional(), + include_domains: z.array(z.string()).optional(), + exclude_domains: z.array(z.string()).optional(), + country: z.string().nullable().optional(), +}); + +export type TavilySearchInput = z.infer; + +// Output schema +export const TavilySearchResultSchema = z.object({ + title: z.string(), + url: z.string(), + content: z.string(), + score: z.number(), + raw_content: z.string().nullable(), + favicon: z.string().optional(), +}); + +export const TavilySearchOutputSchema = z.object({ + query: z.string(), + answer: z.string().optional(), + images: z.array(z.string()), + results: z.array(TavilySearchResultSchema), + auto_parameters: z + .object({ + topic: z.any().optional(), + search_depth: z.any().optional(), + }) + .optional(), + response_time: z.number(), + request_id: z.string(), +}); + +export type TavilySearchOutput = z.infer; diff --git a/packages/app/server/src/routers/resource.ts b/packages/app/server/src/routers/resource.ts index 21877bd95..038851927 100644 --- a/packages/app/server/src/routers/resource.ts +++ b/packages/app/server/src/routers/resource.ts @@ -2,67 +2,20 @@ import express, { Request, Response, Router } from 'express'; import path from 'path'; import logger, { logMetric } from '../logger'; import { TavilySearchInputSchema } from '../resources/tavily/types'; -import { calculateTavilySearchCost, tavilySearch, createTavilyTransaction } from '../resources/tavily/tavily'; +import { + calculateTavilySearchCost, + tavilySearch, + createTavilyTransaction, +} from '../resources/tavily/tavily'; import { buildX402Response, isApiRequest, isX402Request } from 'utils'; import { authenticateRequest } from 'auth'; import { prisma } from 'server'; -import { settle, finalize } from 'handlers'; +import { settle, finalize } from 'handlers'; +import { tavilySearchRoute } from '../resources/tavily/route'; const resourceRouter: Router = Router(); resourceRouter.post('/tavily/search', async (req: Request, res: Response) => { - try { - const headers = req.headers as Record; - - const inputBody = TavilySearchInputSchema.parse(req.body); - - const maxCost = calculateTavilySearchCost(inputBody); - - if ( - !isApiRequest(headers) && - !isX402Request(headers) - ) { - buildX402Response(req, res, maxCost); - return; - } - - 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) { - buildX402Response(req, res, maxCost); - return; - } - 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); - } else { - buildX402Response(req, res, maxCost); - return; - } - } catch (error) { - logger.error('Error searching tavily', error); - return res.status(500).json({ error: 'Internal server error' }); - } + return await tavilySearchRoute(req, res); }); - - - -export default resourceRouter; \ No newline at end of file +export default resourceRouter; diff --git a/packages/app/server/src/schema/chat/completions.ts b/packages/app/server/src/schema/chat/completions.ts index e6e9427b5..d33a05ac5 100644 --- a/packages/app/server/src/schema/chat/completions.ts +++ b/packages/app/server/src/schema/chat/completions.ts @@ -2,19 +2,21 @@ import { z } from 'zod'; import { ALL_SUPPORTED_MODELS } from 'services/AccountingService'; const ChatMessage = z.object({ - role: z.enum(["system", "user", "assistant", "function"]), - content: z.string().optional(), - name: z.string().optional(), // only used when role = “function” or “assistant” sometimes - function_call: z - .object({ - name: z.string(), - arguments: z.string().optional(), - }) - .optional(), - }); + role: z.enum(['system', 'user', 'assistant', 'function']), + content: z.string().optional(), + name: z.string().optional(), // only used when role = “function” or “assistant” sometimes + function_call: z + .object({ + name: z.string(), + arguments: z.string().optional(), + }) + .optional(), +}); export const ChatCompletionInput = z.object({ - model: z.enum(ALL_SUPPORTED_MODELS.map(model => model.model_id) as [string, ...string[]]), + model: z.enum( + ALL_SUPPORTED_MODELS.map(model => model.model_id) as [string, ...string[]] + ), messages: z.array(ChatMessage), // optional parameters @@ -40,17 +42,14 @@ export const ChatCompletionInput = z.object({ .optional(), function_call: z - .union([ - z.enum(["none", "auto"]), - z.object({ name: z.string() }), - ]) + .union([z.enum(['none', 'auto']), z.object({ name: z.string() })]) .optional(), // new structured output / response_format response_format: z .object({ - type: z.enum(["json_schema"]), - json_schema: z.any(), // you may replace with a more precise JSON Schema type + type: z.enum(['json_schema']), + json_schema: z.any(), // you may replace with a more precise JSON Schema type }) .optional(), }); @@ -63,7 +62,7 @@ const ChatMessageContentPart = z.object({ }); const ChatMessageOutput = z.object({ - role: z.enum(["system", "user", "assistant", "function"]), + role: z.enum(['system', 'user', 'assistant', 'function']), content: z.union([z.string(), z.array(ChatMessageContentPart)]).nullable(), name: z.string().optional(), function_call: z @@ -76,7 +75,7 @@ const ChatMessageOutput = z.object({ .array( z.object({ id: z.string(), - type: z.enum(["function"]), + type: z.enum(['function']), function: z.object({ name: z.string(), arguments: z.string(), @@ -90,7 +89,9 @@ const ChatMessageOutput = z.object({ const ChatCompletionChoice = z.object({ index: z.number(), message: ChatMessageOutput, - finish_reason: z.enum(["stop", "length", "tool_calls", "content_filter", "function_call"]).nullable(), + finish_reason: z + .enum(['stop', 'length', 'tool_calls', 'content_filter', 'function_call']) + .nullable(), logprobs: z .object({ content: z @@ -119,7 +120,7 @@ const ChatCompletionChoice = z.object({ // The full response object export const ChatCompletionOutput = z.object({ id: z.string(), - object: z.literal("chat.completion"), + object: z.literal('chat.completion'), created: z.number(), model: z.string(), choices: z.array(ChatCompletionChoice), @@ -138,4 +139,4 @@ export const ChatCompletionOutput = z.object({ }) .optional(), system_fingerprint: z.string().nullable().optional(), -}); \ No newline at end of file +}); diff --git a/packages/app/server/src/schema/image/gemini.ts b/packages/app/server/src/schema/image/gemini.ts index d7037f337..bc9f04d29 100644 --- a/packages/app/server/src/schema/image/gemini.ts +++ b/packages/app/server/src/schema/image/gemini.ts @@ -1,92 +1,92 @@ -import { z } from "zod"; - +import { z } from 'zod'; // ----- Request schemas ----- // A “Part” in a Content, either text or inline data const PartRequest = z.union([ - z.object({ - text: z.string(), - }), - z.object({ - inlineData: z.object({ - mimeType: z.string().min(1), // like "image/png" - data: z.string().min(1), // base64 string - }), + z.object({ + text: z.string(), + }), + z.object({ + inlineData: z.object({ + mimeType: z.string().min(1), // like "image/png" + data: z.string().min(1), // base64 string }), - ]); - - // A “Content” in the “contents” list - const ContentRequest = z.object({ - // optional: in conversational APIs you might include role - role: z.enum(["user", "assistant", "system"]).optional(), - parts: z.array(PartRequest).nonempty(), - }); - - // Optional “generationConfig” section (for structured output, etc.) - const GenerationConfigRequest = z - .object({ - // e.g. to ask for JSON response - responseMimeType: z.string().optional(), - // other config options may exist (temperature, top_p, etc.) - // we mark these optionally - temperature: z.number().optional(), - topP: z.number().optional(), - // etc. - }) - .partial(); - - // Full request body - export const GeminiFlashImageInputSchema = z.object({ - contents: z.array(ContentRequest).nonempty(), - generationConfig: GenerationConfigRequest.optional(), - }); + }), +]); + +// A “Content” in the “contents” list +const ContentRequest = z.object({ + // optional: in conversational APIs you might include role + role: z.enum(['user', 'assistant', 'system']).optional(), + parts: z.array(PartRequest).nonempty(), +}); +// Optional “generationConfig” section (for structured output, etc.) +const GenerationConfigRequest = z + .object({ + // e.g. to ask for JSON response + responseMimeType: z.string().optional(), + // other config options may exist (temperature, top_p, etc.) + // we mark these optionally + temperature: z.number().optional(), + topP: z.number().optional(), + // etc. + }) + .partial(); + +// Full request body +export const GeminiFlashImageInputSchema = z.object({ + contents: z.array(ContentRequest).nonempty(), + generationConfig: GenerationConfigRequest.optional(), +}); // A “Part” in the response const PartResponse = z.union([ - z.object({ - text: z.string().optional(), // might or might not include text - }), - z.object({ - inlineData: z.object({ - mimeType: z.string().optional(), - data: z.string(), // base64-encoded data - }), + z.object({ + text: z.string().optional(), // might or might not include text + }), + z.object({ + inlineData: z.object({ + mimeType: z.string().optional(), + data: z.string(), // base64-encoded data }), - ]); - - const ContentResponse = z.object({ - parts: z.array(PartResponse).nonempty(), - }); - - // A “Candidate” (one possible completed content) - const CandidateResponse = z.object({ - content: ContentResponse, - }); - - // Metadata and auxiliary fields in the response - const PromptFeedback = z - .object({ - blockReason: z.string().optional(), - safetyRatings: z.array(z.object({})).optional(), // you can expand if you know structure - blockReasonMessage: z.string().optional(), - }) - .partial(); // may or may not appear - - const UsageMetadata = z.object({ + }), +]); + +const ContentResponse = z.object({ + parts: z.array(PartResponse).nonempty(), +}); + +// A “Candidate” (one possible completed content) +const CandidateResponse = z.object({ + content: ContentResponse, +}); + +// Metadata and auxiliary fields in the response +const PromptFeedback = z + .object({ + blockReason: z.string().optional(), + safetyRatings: z.array(z.object({})).optional(), // you can expand if you know structure + blockReasonMessage: z.string().optional(), + }) + .partial(); // may or may not appear + +const UsageMetadata = z + .object({ promptTokenCount: z.number().optional(), candidatesTokenCount: z.number().optional(), totalTokenCount: z.number().optional(), // other usage fields could go here - }).partial(); - - // Full response body + }) + .partial(); + +// Full response body export const GeminiFlashImageOutputSchema = z.object({ - candidates: z.array(CandidateResponse).nonempty(), - promptFeedback: PromptFeedback.optional(), - usageMetadata: UsageMetadata.optional(), - // add fields from GenerateContentResponse spec if needed (timestamps, etc.) - // e.g. createTime, etc. - createTime: z.string().optional(), - }); \ No newline at end of file + candidates: z.array(CandidateResponse).nonempty(), + promptFeedback: PromptFeedback.optional(), + usageMetadata: UsageMetadata.optional(), + // add fields from GenerateContentResponse spec if needed (timestamps, etc.) + // e.g. createTime, etc. + createTime: z.string().optional(), +}); diff --git a/packages/app/server/src/schema/image/openai.ts b/packages/app/server/src/schema/image/openai.ts index ff1d65bf6..7e01052e4 100644 --- a/packages/app/server/src/schema/image/openai.ts +++ b/packages/app/server/src/schema/image/openai.ts @@ -1,10 +1,10 @@ -import { z } from "zod"; +import { z } from 'zod'; /** Allowed image sizes, per docs */ -const ImageSize = z.enum(["256x256", "512x512", "1024x1024"]); +const ImageSize = z.enum(['256x256', '512x512', '1024x1024']); /** Allowed response formats */ -const ResponseFormat = z.enum(["url", "b64_json"]); +const ResponseFormat = z.enum(['url', 'b64_json']); /** Create Images API: request (input) */ export const CreateImagesRequest = z.object({ diff --git a/packages/app/server/src/schema/schemaForRoute.ts b/packages/app/server/src/schema/schemaForRoute.ts index 8ee2cf6f9..7e0fe9bf8 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -9,11 +9,12 @@ import { import { z } from 'zod'; import { ChatCompletionInput, ChatCompletionOutput } from './chat/completions'; import { CreateImagesRequest, CreateImagesResponse } from './image/openai'; -import { TavilySearchInputSchema, TavilySearchOutputSchema } from 'resources/tavily/types'; +import { + TavilySearchInputSchema, + TavilySearchOutputSchema, +} from 'resources/tavily/types'; -export function getSchemaForRoute( - path: string -): +export function getSchemaForRoute(path: string): | { input: { type: 'http'; method: string; bodyFields?: unknown }; output: unknown; @@ -83,18 +84,22 @@ export function getSchemaForRoute( output: outputSchema.properties, }; } - if (path.endsWith("/tavily/search")) { - const inputSchema = z.toJSONSchema(TavilySearchInputSchema, { target: "openapi-3.0" }); - const outputSchema = z.toJSONSchema(TavilySearchOutputSchema, { target: "openapi-3.0" }); + if (path.endsWith('/tavily/search')) { + const inputSchema = z.toJSONSchema(TavilySearchInputSchema, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(TavilySearchOutputSchema, { + target: 'openapi-3.0', + }); return { - input: { - type: "http", - method: "POST", - bodyFields: inputSchema.properties, - }, - output: outputSchema.properties, + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, }; -} - + } + return undefined; } diff --git a/packages/app/server/src/schema/video/openai.ts b/packages/app/server/src/schema/video/openai.ts index 604aec90e..ecc3bb8d9 100644 --- a/packages/app/server/src/schema/video/openai.ts +++ b/packages/app/server/src/schema/video/openai.ts @@ -1,29 +1,31 @@ -import { z } from "zod"; +import { z } from 'zod'; -const modelSchema = z.enum(["sora-2" ,"sora-2-pro"]); +const modelSchema = z.enum(['sora-2', 'sora-2-pro']); export const OpenAIVideoCreateParamsSchema = z.object({ - model: modelSchema, - prompt: z.string().nonoptional(), - seconds: z.union([z.literal(4), z.literal(8), z.literal(12)]), - size: z.string().optional(), + model: modelSchema, + prompt: z.string().nonoptional(), + seconds: z.union([z.literal(4), z.literal(8), z.literal(12)]), + size: z.string().optional(), }); export const OpenAIVideoSchema = z.object({ - id: z.string(), - object: z.literal("video"), - status: z.enum(["queued", "in_progress", "completed", "failed"]), - progress: z.number().min(0).max(100).optional(), - created_at: z.number(), - completed_at: z.number().nullable(), - expires_at: z.number().optional(), - model: modelSchema, - remixed_from_video_id: z.string().optional(), - seconds: z.union([z.literal(4), z.literal(8), z.literal(12)]).optional(), - size: z.string().optional(), - error: z.object({ - code: z.string(), - message: z.string(), - param: z.string().nullable().optional(), - }).nullable(), -}); \ No newline at end of file + id: z.string(), + object: z.literal('video'), + status: z.enum(['queued', 'in_progress', 'completed', 'failed']), + progress: z.number().min(0).max(100).optional(), + created_at: z.number(), + completed_at: z.number().nullable(), + expires_at: z.number().optional(), + model: modelSchema, + remixed_from_video_id: z.string().optional(), + seconds: z.union([z.literal(4), z.literal(8), z.literal(12)]).optional(), + size: z.string().optional(), + error: z + .object({ + code: z.string(), + message: z.string(), + param: z.string().nullable().optional(), + }) + .nullable(), +}); diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index 106dcea13..117bc4906 100644 --- a/packages/app/server/src/services/AccountingService.ts +++ b/packages/app/server/src/services/AccountingService.ts @@ -124,7 +124,10 @@ export const getCostPerToken = ( if (!modelPrice) { throw new Error(`Pricing information not found for model: ${model}`); } - if (modelPrice.input_cost_per_token < 0 || modelPrice.output_cost_per_token < 0) { + if ( + modelPrice.input_cost_per_token < 0 || + modelPrice.output_cost_per_token < 0 + ) { throw new Error(`Invalid pricing for model: ${model}`); } diff --git a/packages/sdk/aix402/README.md b/packages/sdk/aix402/README.md index 1c1ae0025..cf8f98f95 100644 --- a/packages/sdk/aix402/README.md +++ b/packages/sdk/aix402/README.md @@ -15,10 +15,12 @@ React hook for automatic x402 payment handling: ```typescript import { useChatWithPayment } from '@merit-systems/ai-x402/client'; -const { messages, input, handleInputChange, handleSubmit } = useChatWithPayment({ - api: '/api/chat', - walletClient: yourWalletClient, -}); +const { messages, input, handleInputChange, handleSubmit } = useChatWithPayment( + { + api: '/api/chat', + walletClient: yourWalletClient, + } +); ``` ## Server Usage diff --git a/packages/sdk/aix402/package.json b/packages/sdk/aix402/package.json index 55c19d3ff..26992c17e 100644 --- a/packages/sdk/aix402/package.json +++ b/packages/sdk/aix402/package.json @@ -72,4 +72,3 @@ "react": "^18.0.0" } } - diff --git a/packages/sdk/aix402/src/client.ts b/packages/sdk/aix402/src/client.ts index 35dc5c555..5cc2e555a 100644 --- a/packages/sdk/aix402/src/client.ts +++ b/packages/sdk/aix402/src/client.ts @@ -2,4 +2,3 @@ export { useChatWithPayment } from './useChatWithPayment'; export type { default as UseChatWithPayment } from './useChatWithPayment'; - diff --git a/packages/sdk/aix402/src/fetch-no-payment.ts b/packages/sdk/aix402/src/fetch-no-payment.ts index 0c79fb6b0..b938dd9f8 100644 --- a/packages/sdk/aix402/src/fetch-no-payment.ts +++ b/packages/sdk/aix402/src/fetch-no-payment.ts @@ -1,40 +1,37 @@ -import { createOpenAI, OpenAIProvider } from "@ai-sdk/openai"; +import { createOpenAI, OpenAIProvider } from '@ai-sdk/openai'; - -function fetchAddPayment(originalFetch: typeof fetch, paymentAuthHeader: string | null | undefined) { - return async (input: RequestInfo | URL, init?: RequestInit) => { - const headers: Record = { ...init?.headers }; - if (paymentAuthHeader) { - headers['x-payment'] = paymentAuthHeader; - } - delete headers['Authorization']; - delete headers['authorization']; - return originalFetch(input, { - ...init, - headers, - }); +function fetchAddPayment( + originalFetch: typeof fetch, + paymentAuthHeader: string | null | undefined +) { + return async (input: RequestInfo | URL, init?: RequestInit) => { + const headers: Record = { ...init?.headers }; + if (paymentAuthHeader) { + headers['x-payment'] = paymentAuthHeader; } - } - - - export function createX402OpenAIWithoutPayment( - paymentAuthHeader?: string | null, - baseRouterUrl?: string, - ): OpenAIProvider { - return createOpenAI({ - baseURL: baseRouterUrl || 'https://echo.router.merit.systems', - apiKey: 'placeholder_replaced_by_fetchAddPayment', - fetch: fetchAddPayment( - fetch, - paymentAuthHeader, - ), + delete headers['Authorization']; + delete headers['authorization']; + return originalFetch(input, { + ...init, + headers, }); - } - + }; +} + +export function createX402OpenAIWithoutPayment( + paymentAuthHeader?: string | null, + baseRouterUrl?: string +): OpenAIProvider { + return createOpenAI({ + baseURL: baseRouterUrl || 'https://echo.router.merit.systems', + apiKey: 'placeholder_replaced_by_fetchAddPayment', + fetch: fetchAddPayment(fetch, paymentAuthHeader), + }); +} export function UiStreamOnError(): (error: any) => string { - return (error) => { - const errorBody = error as { responseBody: string } - return errorBody.responseBody - } + return error => { + const errorBody = error as { responseBody: string }; + return errorBody.responseBody; + }; } diff --git a/packages/sdk/aix402/src/fetch-with-payment.ts b/packages/sdk/aix402/src/fetch-with-payment.ts index 252cfd78a..fa1c44cc2 100644 --- a/packages/sdk/aix402/src/fetch-with-payment.ts +++ b/packages/sdk/aix402/src/fetch-with-payment.ts @@ -1,68 +1,68 @@ -import { createOpenAI, OpenAIProvider } from "@ai-sdk/openai"; -import { createPaymentHeader, selectPaymentRequirements } from "x402/client"; -import { PaymentRequirementsSchema, Signer } from "x402/types"; +import { createOpenAI, OpenAIProvider } from '@ai-sdk/openai'; +import { createPaymentHeader, selectPaymentRequirements } from 'x402/client'; +import { PaymentRequirementsSchema, Signer } from 'x402/types'; -export async function getPaymentHeaderFromBody(body: any, walletClient: Signer) { +export async function getPaymentHeaderFromBody( + body: any, + walletClient: Signer +) { + const { x402Version, accepts } = body as { + x402Version: number; + accepts: unknown[]; + }; + const parsedPaymentRequirements = accepts.map(x => + PaymentRequirementsSchema.parse(x) + ); + const selectedPaymentRequirements = selectPaymentRequirements( + parsedPaymentRequirements + ); - const { x402Version, accepts } = (body) as { - x402Version: number; - accepts: unknown[]; - }; - const parsedPaymentRequirements = accepts.map(x => PaymentRequirementsSchema.parse(x)); - - - const selectedPaymentRequirements = selectPaymentRequirements(parsedPaymentRequirements); - - - const paymentHeader = await createPaymentHeader( - walletClient, - x402Version, - selectedPaymentRequirements - ); - return paymentHeader; - } + const paymentHeader = await createPaymentHeader( + walletClient, + x402Version, + selectedPaymentRequirements + ); + return paymentHeader; +} +function fetchWithX402Payment(fetch: any, walletClient: Signer): typeof fetch { + return async (input: URL, init?: RequestInit) => { + const headers: Record = { ...init?.headers }; + delete headers['Authorization']; + delete headers['authorization']; - function fetchWithX402Payment(fetch: any, walletClient: Signer): typeof fetch { - return async (input: URL, init?: RequestInit) => { - const headers: Record = { ...init?.headers }; - - delete headers['Authorization']; - delete headers['authorization']; + const response = await fetch(input, { + ...init, + headers, + }); - const response = await fetch(input, { + if (response.status === 402) { + const paymentRequiredJson = await response.json(); + const paymentHeader = await getPaymentHeaderFromBody( + paymentRequiredJson, + walletClient + ); + headers['x-payment'] = paymentHeader; + const newResponse = await fetch(input, { ...init, headers, }); - - if (response.status === 402) { - const paymentRequiredJson = await response.json(); - const paymentHeader = await getPaymentHeaderFromBody(paymentRequiredJson, walletClient); - headers['x-payment'] = paymentHeader; - const newResponse = await fetch(input, { - ...init, - headers, - }); - return newResponse; - } - - - return response; + return newResponse; } - } - - export function createX402OpenAI( - walletClient: Signer, - baseRouterUrl?: string, - ): OpenAIProvider { - return createOpenAI({ - baseURL: baseRouterUrl || 'https://echo.router.merit.systems', - apiKey: 'placeholder_replaced_by_echoFetch', - fetch: fetchWithX402Payment( - fetch, - walletClient - ), - }); - } \ No newline at end of file + + return response; + }; +} + +export function createX402OpenAI( + walletClient: Signer, + baseRouterUrl?: string +): OpenAIProvider { + return createOpenAI({ + baseURL: baseRouterUrl || 'https://echo.router.merit.systems', + apiKey: 'placeholder_replaced_by_echoFetch', + fetch: fetchWithX402Payment(fetch, walletClient), + }); +} diff --git a/packages/sdk/aix402/src/server.ts b/packages/sdk/aix402/src/server.ts index 138705a44..71238b4c9 100644 --- a/packages/sdk/aix402/src/server.ts +++ b/packages/sdk/aix402/src/server.ts @@ -1,2 +1,5 @@ -export { createX402OpenAIWithoutPayment, UiStreamOnError } from './fetch-no-payment'; +export { + createX402OpenAIWithoutPayment, + UiStreamOnError, +} from './fetch-no-payment'; export { createX402OpenAI } from './fetch-with-payment'; diff --git a/packages/sdk/aix402/src/useChatWithPayment.ts b/packages/sdk/aix402/src/useChatWithPayment.ts index fbe79929d..c41c5ade9 100644 --- a/packages/sdk/aix402/src/useChatWithPayment.ts +++ b/packages/sdk/aix402/src/useChatWithPayment.ts @@ -1,28 +1,38 @@ 'use client'; import { useEffect, useRef, RefObject } from 'react'; -import { useChat, type UseChatHelpers, type UseChatOptions, type UIMessage } from '@ai-sdk/react'; +import { + useChat, + type UseChatHelpers, + type UseChatOptions, + type UIMessage, +} from '@ai-sdk/react'; import { ChatInit } from 'ai'; import { PaymentRequirementsSchema, type Signer } from 'x402/types'; import { createPaymentHeader } from 'x402/client'; import { handleX402Error } from './utils'; -type UseChatWithPaymentParams = ChatInit & UseChatOptions & { - walletClient: Signer; - regenerateOptions?: any; -}; +type UseChatWithPaymentParams = + ChatInit & + UseChatOptions & { + walletClient: Signer; + regenerateOptions?: any; + }; async function handlePaymentError( - err: Error, - regenerate: UseChatHelpers["regenerate"], regenerateOptions: any, - walletClientRef: RefObject) { - + err: Error, + regenerate: UseChatHelpers['regenerate'], + regenerateOptions: any, + walletClientRef: RefObject +) { const paymentDetails = handleX402Error(err); if (!paymentDetails) { return; } const currentWalletClient = walletClientRef.current; - const paymentRequirement = PaymentRequirementsSchema.parse(paymentDetails.accepts[0]); + const paymentRequirement = PaymentRequirementsSchema.parse( + paymentDetails.accepts[0] + ); const paymentHeader = await createPaymentHeader( currentWalletClient as unknown as Signer, paymentDetails.x402Version, @@ -34,12 +44,13 @@ async function handlePaymentError( ...regenerateOptions, }); } - } -export function useChatWithPayment( - { walletClient, regenerateOptions, ...options }: UseChatWithPaymentParams -): UseChatHelpers { +export function useChatWithPayment({ + walletClient, + regenerateOptions, + ...options +}: UseChatWithPaymentParams): UseChatHelpers { const walletClientRef = useRef(walletClient); useEffect(() => { @@ -47,18 +58,17 @@ export function useChatWithPayment( }, [walletClient]); const { regenerate, ...chat } = useChat({ - ...(options), + ...options, onError: async (err: Error) => { - if (options.onError) options.onError(err); + if (options.onError) options.onError(err); handlePaymentError(err, regenerate, regenerateOptions, walletClientRef); }, }); return { ...chat, - regenerate + regenerate, }; } export default useChatWithPayment; - diff --git a/packages/sdk/aix402/src/utils.ts b/packages/sdk/aix402/src/utils.ts index 6081ed4cb..727986a91 100644 --- a/packages/sdk/aix402/src/utils.ts +++ b/packages/sdk/aix402/src/utils.ts @@ -1,6 +1,8 @@ import { z } from 'zod'; -const evmAddress = z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address'); +const evmAddress = z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address'); const hexString = z.string().regex(/^0x[a-fA-F0-9]+$/, 'Invalid hex string'); const decimalString = z.string().regex(/^\d+$/, 'Must be a decimal string'); @@ -47,7 +49,7 @@ export function handleX402Error(error: Error): X402PaymentDetails | false { const paymentDetails = X402PaymentDetailsSchema.parse(parsedError); return paymentDetails; } catch (error) { - console.error("error: ", error) - return false + console.error('error: ', error); + return false; } -} \ No newline at end of file +} diff --git a/packages/sdk/aix402/tsconfig.json b/packages/sdk/aix402/tsconfig.json index 7227bb8f4..89295a77a 100644 --- a/packages/sdk/aix402/tsconfig.json +++ b/packages/sdk/aix402/tsconfig.json @@ -10,4 +10,3 @@ "include": ["src/**/*"], "exclude": ["node_modules", "dist", "__tests__/**/*"] } - diff --git a/packages/sdk/aix402/tsup.config.ts b/packages/sdk/aix402/tsup.config.ts index dcc62e430..e53f795d8 100644 --- a/packages/sdk/aix402/tsup.config.ts +++ b/packages/sdk/aix402/tsup.config.ts @@ -11,4 +11,3 @@ export default defineConfig({ splitting: false, bundle: true, }); - diff --git a/packages/sdk/aix402/vitest.config.ts b/packages/sdk/aix402/vitest.config.ts index 346b909cb..d484f5930 100644 --- a/packages/sdk/aix402/vitest.config.ts +++ b/packages/sdk/aix402/vitest.config.ts @@ -15,4 +15,3 @@ export default defineConfig({ globals: true, }, }); - diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index 3aa7bd68c..aca35fe81 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -1,6 +1,15 @@ #!/usr/bin/env node -import { intro, outro, select, text, spinner, log, isCancel, cancel } from '@clack/prompts'; +import { + intro, + outro, + select, + text, + spinner, + log, + isCancel, + cancel, +} from '@clack/prompts'; import chalk from 'chalk'; import { Command } from 'commander'; import degit from 'degit'; @@ -82,7 +91,10 @@ function detectPackageManager(): PackageManager { return 'pnpm'; } -function getPackageManagerCommands(pm: PackageManager): { install: string; dev: string } { +function getPackageManagerCommands(pm: PackageManager): { + install: string; + dev: string; +} { switch (pm) { case 'pnpm': return { install: 'pnpm install', dev: 'pnpm dev' }; @@ -114,7 +126,7 @@ async function runInstall( projectPath: string, onProgress?: (line: string) => void ): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { const command = packageManager; const args = ['install']; @@ -125,7 +137,7 @@ async function runInstall( let lastLine = ''; - child.stdout?.on('data', (data) => { + child.stdout?.on('data', data => { const lines = data.toString().split('\n'); const relevantLine = lines .filter((line: string) => line.trim().length > 0) @@ -141,7 +153,7 @@ async function runInstall( } }); - child.on('close', (code) => { + child.on('close', code => { resolve(code === 0); }); @@ -170,11 +182,13 @@ async function createApp(projectDir: string, options: CreateAppOptions) { if (!template) { const selectedTemplate = await select({ message: 'Which template would you like to use?', - options: Object.entries(DEFAULT_TEMPLATES).map(([key, { title, description }]) => ({ - label: title, - hint: description, - value: key, - })), + options: Object.entries(DEFAULT_TEMPLATES).map( + ([key, { title, description }]) => ({ + label: title, + hint: description, + value: key, + }) + ), }); if (isCancel(selectedTemplate)) { @@ -272,7 +286,9 @@ async function createApp(projectDir: string, options: CreateAppOptions) { const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); packageJson.name = toSafePackageName(projectDir); writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - log.message(`Updated package.json with project name: ${toSafePackageName(projectDir)}`); + log.message( + `Updated package.json with project name: ${toSafePackageName(projectDir)}` + ); } // Update .env.local with the provided app ID @@ -310,8 +326,10 @@ async function createApp(projectDir: string, options: CreateAppOptions) { const installSuccess = await runInstall( packageManager, absoluteProjectPath, - (progressLine) => { - s.message(`Installing dependencies with ${packageManager}... ${chalk.gray(progressLine + '...')}`); + progressLine => { + s.message( + `Installing dependencies with ${packageManager}... ${chalk.gray(progressLine + '...')}` + ); } ); @@ -319,7 +337,9 @@ async function createApp(projectDir: string, options: CreateAppOptions) { s.stop('Dependencies installed successfully'); } else { s.stop('Failed to install dependencies'); - log.warning(`Could not install dependencies with ${packageManager}. Please run manually.`); + log.warning( + `Could not install dependencies with ${packageManager}. Please run manually.` + ); } } @@ -328,9 +348,9 @@ async function createApp(projectDir: string, options: CreateAppOptions) { ? [`cd ${projectDir}`, install, dev] : [`cd ${projectDir}`, dev]; - const nextSteps = `${chalk.cyan('Get started:')}\n` + steps - .map(step => ` ${chalk.cyan('└')} ${step}`) - .join('\n'); + const nextSteps = + `${chalk.cyan('Get started:')}\n` + + steps.map(step => ` ${chalk.cyan('└')} ${step}`).join('\n'); outro(`Success! Created ${projectDir}\n\n${nextSteps}`); @@ -338,9 +358,13 @@ async function createApp(projectDir: string, options: CreateAppOptions) { } catch (error) { if (error instanceof Error) { if (error.message.includes('could not find commit hash')) { - cancel(`Template "${template}" not found in repository.\n\nThe template might not exist yet. Please check:\nhttps://github.com/Merit-Systems/echo/tree/master/templates`); + cancel( + `Template "${template}" not found in repository.\n\nThe template might not exist yet. Please check:\nhttps://github.com/Merit-Systems/echo/tree/master/templates` + ); } else if (error.message.includes('Repository does not exist')) { - cancel('Repository not accessible.\n\nMake sure you have access to the Merit-Systems/echo repository.'); + cancel( + 'Repository not accessible.\n\nMake sure you have access to the Merit-Systems/echo repository.' + ); } else { cancel(`Failed to create app: ${error.message}`); } diff --git a/packages/sdk/examples/next-402-chat/package.json b/packages/sdk/examples/next-402-chat/package.json index 1af76d8f5..0b546ee29 100644 --- a/packages/sdk/examples/next-402-chat/package.json +++ b/packages/sdk/examples/next-402-chat/package.json @@ -72,4 +72,4 @@ "@ai-sdk/openai": "2.0.16" } } -} \ No newline at end of file +} diff --git a/packages/sdk/examples/next-402-chat/src/app/_components/chat-no-payment.tsx b/packages/sdk/examples/next-402-chat/src/app/_components/chat-no-payment.tsx index b2aba19a4..2aabd51ba 100644 --- a/packages/sdk/examples/next-402-chat/src/app/_components/chat-no-payment.tsx +++ b/packages/sdk/examples/next-402-chat/src/app/_components/chat-no-payment.tsx @@ -61,32 +61,36 @@ const ChatBotDemo = () => { const [model, setModel] = useState(models[0].value); const pendingMessageRef = useRef(null); - const { messages, sendMessage, status } = useChat( - - ); + const { messages, sendMessage, status } = useChat(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { pendingMessageRef.current = input; - sendMessage({ text: input }, { - body: { - model: model, - useServerWallet: true, - }, - }); + sendMessage( + { text: input }, + { + body: { + model: model, + useServerWallet: true, + }, + } + ); setInput(''); } }; const handleSuggestionClick = async (suggestion: string) => { pendingMessageRef.current = suggestion; - sendMessage({ text: suggestion}, { - body: { - model: model, - useServerWallet: true, - }, - }); + sendMessage( + { text: suggestion }, + { + body: { + model: model, + useServerWallet: true, + }, + } + ); }; return ( diff --git a/packages/sdk/examples/next-402-chat/src/app/_components/chat.tsx b/packages/sdk/examples/next-402-chat/src/app/_components/chat.tsx index 89c640b31..85d616d56 100644 --- a/packages/sdk/examples/next-402-chat/src/app/_components/chat.tsx +++ b/packages/sdk/examples/next-402-chat/src/app/_components/chat.tsx @@ -71,7 +71,7 @@ const ChatBotDemo = () => { }, }, onError: (error: any) => { - console.error("error: ", error) + console.error('error: ', error); }, }); const handleSubmit = async (e: React.FormEvent) => { @@ -79,22 +79,28 @@ const ChatBotDemo = () => { if (input.trim()) { pendingMessageRef.current = input; - sendMessage({ text: input }, { - body: { - model: model, - }, - }); + sendMessage( + { text: input }, + { + body: { + model: model, + }, + } + ); setInput(''); } }; const handleSuggestionClick = async (suggestion: string) => { pendingMessageRef.current = suggestion; - sendMessage({ text: suggestion}, { - body: { - model: model, - }, - }); + sendMessage( + { text: suggestion }, + { + body: { + model: model, + }, + } + ); }; return ( diff --git a/packages/sdk/examples/next-402-chat/src/app/_components/header.tsx b/packages/sdk/examples/next-402-chat/src/app/_components/header.tsx index 25acb41ff..8ccb8fe0b 100644 --- a/packages/sdk/examples/next-402-chat/src/app/_components/header.tsx +++ b/packages/sdk/examples/next-402-chat/src/app/_components/header.tsx @@ -6,10 +6,7 @@ interface HeaderProps { className?: string; } -const Header: FC = ({ - title = 'My App', - className = '', -}) => { +const Header: FC = ({ title = 'My App', className = '' }) => { return (