From e30228f6153ee200206138b474762c3053cf0615 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Thu, 9 Oct 2025 12:11:19 -0400 Subject: [PATCH 01/50] 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 02/50] 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 03/50] 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 89b3451dd1c3cff96406ab2a24bfe5cfc72bd4b6 Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Fri, 10 Oct 2025 14:09:33 -0400 Subject: [PATCH 04/50] db changes --- .../20251010175808_add_x402_video_gen/migration.sql | 11 +++++++++++ packages/app/control/prisma/schema.prisma | 10 ++++++++++ 2 files changed, 21 insertions(+) create mode 100644 packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql diff --git a/packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql b/packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql new file mode 100644 index 000000000..3f281e016 --- /dev/null +++ b/packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "VideoGenerationX402" ( + "id" UUID NOT NULL, + "videoId" TEXT NOT NULL, + "wallet" TEXT, + "cost" DECIMAL(65,30) NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isFinal" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "VideoGenerationX402_pkey" PRIMARY KEY ("id") +); diff --git a/packages/app/control/prisma/schema.prisma b/packages/app/control/prisma/schema.prisma index 3915c5e21..592ef2ef6 100644 --- a/packages/app/control/prisma/schema.prisma +++ b/packages/app/control/prisma/schema.prisma @@ -489,3 +489,13 @@ model OutboundEmailSent { @@index([emailCampaignId]) @@map("outbound_emails_sent") } + +model VideoGenerationX402 { + id String @id @default(uuid()) @db.Uuid + videoId String + wallet String? + cost Decimal + createdAt DateTime @default(now()) @db.Timestamptz(6) + status String + isFinal Boolean @default(false) +} From 3278e3f136fdaa742710b20f212cfb8713c077f4 Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Fri, 10 Oct 2025 14:40:57 -0400 Subject: [PATCH 05/50] implement refund logic for failed sora requests --- packages/app/control/prisma/schema.prisma | 1 - packages/app/server/src/handlers.ts | 11 ++++ .../src/providers/OpenAIVideoProvider.ts | 50 ++++++++++++++++++- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/app/control/prisma/schema.prisma b/packages/app/control/prisma/schema.prisma index 592ef2ef6..9ec9170e3 100644 --- a/packages/app/control/prisma/schema.prisma +++ b/packages/app/control/prisma/schema.prisma @@ -496,6 +496,5 @@ model VideoGenerationX402 { wallet String? cost Decimal createdAt DateTime @default(now()) @db.Timestamptz(6) - status String isFinal Boolean @default(false) } diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index b63182899..723b93daf 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -23,6 +23,7 @@ import { } from 'services/facilitator/x402-types'; import { Decimal } from '@prisma/client/runtime/library'; import logger from 'logger'; +import { ProviderType } from 'providers/ProviderType'; export async function handleX402Request({ req, @@ -111,6 +112,16 @@ export async function handleX402Request({ transaction = transactionResult.transaction; data = transactionResult.data; + if (provider.getType() === ProviderType.OPENAI_VIDEOS) { + await prisma.videoGenerationX402.create({ + data: { + videoId: transaction.metadata.providerId, + wallet: payload.authorization.from, + cost: transaction.rawTransactionCost, + }, + }); + } + // Send the response - the middleware has intercepted res.end()/res.json() // and will actually send it after settlement completes modelRequestService.handleResolveResponse(res, isStream, data); diff --git a/packages/app/server/src/providers/OpenAIVideoProvider.ts b/packages/app/server/src/providers/OpenAIVideoProvider.ts index 873fc3caf..1c2b690c9 100644 --- a/packages/app/server/src/providers/OpenAIVideoProvider.ts +++ b/packages/app/server/src/providers/OpenAIVideoProvider.ts @@ -4,6 +4,7 @@ import { Request } from 'express'; import { ProviderType } from './ProviderType'; import { EscrowRequest } from '../middleware/transaction-escrow-middleware'; import { Response } from 'express'; +import { transfer } from 'transferWithAuth'; import { getVideoModelPrice } from 'services/AccountingService'; import { HttpError, UnknownModelError } from 'errors/http'; import { Decimal } from 'generated/prisma/runtime/library'; @@ -11,6 +12,7 @@ import { Transaction } from '../types'; import { prisma } from '../server'; import { EchoDbService } from '../services/DbService'; import logger from '../logger'; +import { decimalToUsdcBigInt } from 'utils'; export class OpenAIVideoProvider extends BaseProvider { static detectPassthroughProxy( @@ -173,8 +175,52 @@ export class OpenAIVideoProvider extends BaseProvider { } const responseData = await response.json(); - res.json(responseData); - } + switch (responseData.status) { + case 'completed': + const videoId = responseData.id as string; + await prisma.videoGenerationX402.updateMany({ + where: { + videoId: videoId, + }, + data: { + isFinal: true, + }, + }); + break; + case 'failed': + const failedVideoId = responseData.id as string; + const existingRecord = await prisma.videoGenerationX402.findFirst({ + where: { + videoId: failedVideoId, + }, + }); + + if (existingRecord?.isFinal) { + break; + } + + if (existingRecord && existingRecord.wallet) { + const refundAmount = decimalToUsdcBigInt(existingRecord.cost); + await transfer( + existingRecord.wallet as `0x${string}`, + refundAmount + ); + await prisma.videoGenerationX402.updateMany({ + where: { + videoId: failedVideoId, + }, + data: { + isFinal: true, + }, + }); + } + break; + default: + break; + } + res.json(responseData); + } + // ========== Video Download Handling ========== From af978d0322db15eb2415d086617ee8e8ca78d7e4 Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Mon, 13 Oct 2025 11:11:26 -0400 Subject: [PATCH 06/50] check in --- packages/app/control/prisma/schema.prisma | 5 +-- .../src/providers/OpenAIVideoProvider.ts | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/app/control/prisma/schema.prisma b/packages/app/control/prisma/schema.prisma index 9ec9170e3..87c7118b6 100644 --- a/packages/app/control/prisma/schema.prisma +++ b/packages/app/control/prisma/schema.prisma @@ -491,10 +491,11 @@ model OutboundEmailSent { } model VideoGenerationX402 { - id String @id @default(uuid()) @db.Uuid - videoId String + videoId String @id wallet String? cost Decimal createdAt DateTime @default(now()) @db.Timestamptz(6) isFinal Boolean @default(false) + + @@map("video_generation_x402") } diff --git a/packages/app/server/src/providers/OpenAIVideoProvider.ts b/packages/app/server/src/providers/OpenAIVideoProvider.ts index 1c2b690c9..2a409e335 100644 --- a/packages/app/server/src/providers/OpenAIVideoProvider.ts +++ b/packages/app/server/src/providers/OpenAIVideoProvider.ts @@ -220,6 +220,37 @@ export class OpenAIVideoProvider extends BaseProvider { } res.json(responseData); } + + // ====== Refund methods ====== + private async handleSuccessfulVideoGeneration(videoId: string): Promise { + await prisma.$transaction(async (tx) => { + const video = await tx.$queryRawUnsafe( + `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, + videoId + ); + if (video && !(video as any).isFinal) { + await tx.videoGenerationX402.update({ + where: { + videoId: (video as any).videoId, + }, + data: { + isFinal: true, + }, + }); + } + }); + } + + private async handleFailedVideoGeneration(videoId: string): Promise { + await prisma.$transaction(async(tx) => { + const video = await tx.$queryRawUnsafe( + `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, + videoId + ); + if (video) { + } + }); + } // ========== Video Download Handling ========== From 54ecfe0dc21dd9ae615bca5ba44a3006c5e5823d Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Mon, 13 Oct 2025 11:15:36 -0400 Subject: [PATCH 07/50] fix migrations --- .../migration.sql | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql diff --git a/packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql b/packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql new file mode 100644 index 000000000..85554210c --- /dev/null +++ b/packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - You are about to drop the `VideoGenerationX402` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "public"."VideoGenerationX402"; + +-- CreateTable +CREATE TABLE "video_generation_x402" ( + "videoId" TEXT NOT NULL, + "wallet" TEXT, + "cost" DECIMAL(65,30) NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isFinal" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "video_generation_x402_pkey" PRIMARY KEY ("videoId") +); From 7c6a51484dd04fd4327096e3c14dcb221de0472a Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Mon, 13 Oct 2025 11:27:15 -0400 Subject: [PATCH 08/50] fix with new atomic logic --- .../app/server/src/providers/BaseProvider.ts | 2 +- .../src/providers/OpenAIVideoProvider.ts | 116 ++++++------- .../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 | 139 +++++++++------- .../app/server/src/schema/video/openai.ts | 48 +++--- packages/app/server/src/utils.ts | 12 +- 8 files changed, 270 insertions(+), 250 deletions(-) diff --git a/packages/app/server/src/providers/BaseProvider.ts b/packages/app/server/src/providers/BaseProvider.ts index b4862901f..f2d59301b 100644 --- a/packages/app/server/src/providers/BaseProvider.ts +++ b/packages/app/server/src/providers/BaseProvider.ts @@ -47,7 +47,7 @@ export abstract class BaseProvider { if (apiKey === undefined || apiKey.length === 0) { throw new Error('No API key found'); } - delete headers["authorization"]; + delete headers['authorization']; return { ...headers, Authorization: `Bearer ${apiKey}`, diff --git a/packages/app/server/src/providers/OpenAIVideoProvider.ts b/packages/app/server/src/providers/OpenAIVideoProvider.ts index 2a409e335..1de72d349 100644 --- a/packages/app/server/src/providers/OpenAIVideoProvider.ts +++ b/packages/app/server/src/providers/OpenAIVideoProvider.ts @@ -177,81 +177,63 @@ export class OpenAIVideoProvider extends BaseProvider { const responseData = await response.json(); switch (responseData.status) { case 'completed': - const videoId = responseData.id as string; - await prisma.videoGenerationX402.updateMany({ - where: { - videoId: videoId, - }, - data: { - isFinal: true, - }, - }); + await this.handleSuccessfulVideoGeneration(responseData.id as string); break; case 'failed': - const failedVideoId = responseData.id as string; - const existingRecord = await prisma.videoGenerationX402.findFirst({ - where: { - videoId: failedVideoId, - }, - }); - - if (existingRecord?.isFinal) { - break; - } - - if (existingRecord && existingRecord.wallet) { - const refundAmount = decimalToUsdcBigInt(existingRecord.cost); - await transfer( - existingRecord.wallet as `0x${string}`, - refundAmount - ); - await prisma.videoGenerationX402.updateMany({ - where: { - videoId: failedVideoId, - }, - data: { - isFinal: true, - }, - }); - } + await this.handleFailedVideoGeneration(responseData.id as string); break; default: - break; - } - res.json(responseData); + break; } + res.json(responseData); + } - // ====== Refund methods ====== - private async handleSuccessfulVideoGeneration(videoId: string): Promise { - await prisma.$transaction(async (tx) => { - const video = await tx.$queryRawUnsafe( - `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, - videoId - ); - if (video && !(video as any).isFinal) { - await tx.videoGenerationX402.update({ - where: { - videoId: (video as any).videoId, - }, - data: { - isFinal: true, - }, - }); - } + // ====== Refund methods ====== + private async handleSuccessfulVideoGeneration( + videoId: string + ): Promise { + await prisma.$transaction(async tx => { + const video = await tx.$queryRawUnsafe( + `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, + videoId + ); + if (video && !(video as any).isFinal) { + await tx.videoGenerationX402.update({ + where: { + videoId: (video as any).videoId, + }, + data: { + isFinal: true, + }, }); - } + } + }); + } - private async handleFailedVideoGeneration(videoId: string): Promise { - await prisma.$transaction(async(tx) => { - const video = await tx.$queryRawUnsafe( - `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, - videoId - ); - if (video) { - } - }); - } - + private async handleFailedVideoGeneration(videoId: string): Promise { + await prisma.$transaction(async tx => { + const video = await tx.$queryRawUnsafe( + `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, + videoId + ); + // Exit early if video already final + if (!video || (video as any).isFinal) { + return; + } + if ((video as any).wallet) { + const refundAmount = decimalToUsdcBigInt((video as any).cost); + await transfer((video as any).wallet as `0x${string}`, refundAmount); + } + await tx.videoGenerationX402.update({ + where: { + videoId: (video as any).videoId, + }, + data: { + isFinal: true, + }, + }); + }); + } // ========== Video Download Handling ========== 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 429fedc4d..8ad4253c8 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -1,57 +1,86 @@ -import { OpenAIVideoCreateParamsSchema, OpenAIVideoSchema } from "./video/openai"; -import { GeminiFlashImageInputSchema, GeminiFlashImageOutputSchema } from "./image/gemini"; -import { z } from "zod"; -import { ChatCompletionInput, ChatCompletionOutput } from "./chat/completions"; -import { CreateImagesRequest, CreateImagesResponse } from "./image/openai"; +import { + OpenAIVideoCreateParamsSchema, + OpenAIVideoSchema, +} from './video/openai'; +import { + GeminiFlashImageInputSchema, + GeminiFlashImageOutputSchema, +} from './image/gemini'; +import { z } from 'zod'; +import { ChatCompletionInput, ChatCompletionOutput } from './chat/completions'; +import { CreateImagesRequest, CreateImagesResponse } from './image/openai'; -export function getSchemaForRoute(path: string): { input: { type: "http"; method: string; bodyFields?: unknown }; output: unknown } | undefined { - if (path.endsWith("/videos")) { - const inputSchema = z.toJSONSchema(OpenAIVideoCreateParamsSchema, { target: "openapi-3.0" }); - const outputSchema = z.toJSONSchema(OpenAIVideoSchema, { target: "openapi-3.0" }); - return { - input: { - type: "http", - method: "POST", - bodyFields: inputSchema.properties, - }, - output: outputSchema.properties, - }; +export function getSchemaForRoute( + path: string +): + | { + input: { type: 'http'; method: string; bodyFields?: unknown }; + output: unknown; } - if (path.endsWith("/images/generations")) { - const inputSchema = z.toJSONSchema(CreateImagesRequest, { target: "openapi-3.0" }); - const outputSchema = z.toJSONSchema(CreateImagesResponse, { target: "openapi-3.0" }); - return { - input: { - type: "http", - method: "POST", - bodyFields: inputSchema.properties, - }, - output: outputSchema.properties, - }; - } - if (path.endsWith(":generateContent")) { - const inputSchema = z.toJSONSchema(GeminiFlashImageInputSchema, { target: "openapi-3.0" }); - const outputSchema = z.toJSONSchema(GeminiFlashImageOutputSchema, { target: "openapi-3.0" }); - return { - input: { - type: "http", - method: "POST", - bodyFields: inputSchema.properties, - }, - output: outputSchema.properties, - }; - } - if (path.endsWith("/chat/completions")) { - const inputSchema = z.toJSONSchema(ChatCompletionInput, { target: "openapi-3.0" }); - const outputSchema = z.toJSONSchema(ChatCompletionOutput, { target: "openapi-3.0" }); - return { - input: { - type: "http", - method: "POST", - bodyFields: inputSchema.properties, - }, - output: outputSchema.properties, - }; - } - return undefined; -} \ No newline at end of file + | undefined { + if (path.endsWith('/videos')) { + const inputSchema = z.toJSONSchema(OpenAIVideoCreateParamsSchema, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(OpenAIVideoSchema, { + target: 'openapi-3.0', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } + if (path.endsWith('/images/generations')) { + const inputSchema = z.toJSONSchema(CreateImagesRequest, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(CreateImagesResponse, { + target: 'openapi-3.0', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } + if (path.endsWith(':generateContent')) { + const inputSchema = z.toJSONSchema(GeminiFlashImageInputSchema, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(GeminiFlashImageOutputSchema, { + target: 'openapi-3.0', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } + if (path.endsWith('/chat/completions')) { + const inputSchema = z.toJSONSchema(ChatCompletionInput, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(ChatCompletionOutput, { + target: 'openapi-3.0', + }); + return { + 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/utils.ts b/packages/app/server/src/utils.ts index 66afe23e3..abbd7bc4c 100644 --- a/packages/app/server/src/utils.ts +++ b/packages/app/server/src/utils.ts @@ -136,13 +136,19 @@ export async function buildX402Response( network, }) ); - + let outputSchema; try { outputSchema = getSchemaForRoute(req.path); - logger.info('Schema generated for route', { path: req.path, hasSchema: !!outputSchema }); + logger.info('Schema generated for route', { + path: req.path, + hasSchema: !!outputSchema, + }); } catch (error) { - logger.error('Failed to generate schema for route', { path: req.path, error }); + logger.error('Failed to generate schema for route', { + path: req.path, + error, + }); outputSchema = undefined; } From b4f7f93f95f894c303b0066e010e52a337b9a213 Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Mon, 13 Oct 2025 13:01:54 -0400 Subject: [PATCH 09/50] tested --- .../src/providers/OpenAIVideoProvider.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/app/server/src/providers/OpenAIVideoProvider.ts b/packages/app/server/src/providers/OpenAIVideoProvider.ts index 1de72d349..aacc906d5 100644 --- a/packages/app/server/src/providers/OpenAIVideoProvider.ts +++ b/packages/app/server/src/providers/OpenAIVideoProvider.ts @@ -193,14 +193,15 @@ export class OpenAIVideoProvider extends BaseProvider { videoId: string ): Promise { await prisma.$transaction(async tx => { - const video = await tx.$queryRawUnsafe( + const result = await tx.$queryRawUnsafe( `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, videoId ); - if (video && !(video as any).isFinal) { + const video = (result as any[])[0]; + if (video && !video.isFinal) { await tx.videoGenerationX402.update({ where: { - videoId: (video as any).videoId, + videoId: video.videoId, }, data: { isFinal: true, @@ -212,21 +213,22 @@ export class OpenAIVideoProvider extends BaseProvider { private async handleFailedVideoGeneration(videoId: string): Promise { await prisma.$transaction(async tx => { - const video = await tx.$queryRawUnsafe( + const result = await tx.$queryRawUnsafe( `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, videoId ); + const video = (result as any[])[0]; // Exit early if video already final - if (!video || (video as any).isFinal) { + if (!video || video.isFinal) { return; } - if ((video as any).wallet) { - const refundAmount = decimalToUsdcBigInt((video as any).cost); - await transfer((video as any).wallet as `0x${string}`, refundAmount); + if (video.wallet) { + const refundAmount = decimalToUsdcBigInt(video.cost); + await transfer(video.wallet as `0x${string}`, refundAmount); } await tx.videoGenerationX402.update({ where: { - videoId: (video as any).videoId, + videoId: video.videoId, }, data: { isFinal: true, From 0ec1dc2931e1460b259e16c41de9546688c31b8c Mon Sep 17 00:00:00 2001 From: sragss Date: Tue, 14 Oct 2025 11:02:09 -0400 Subject: [PATCH 10/50] increase api route limit to 4mb --- templates/next-image/src/app/api/edit-image/route.ts | 8 ++++++++ templates/next-image/src/app/api/generate-image/route.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/templates/next-image/src/app/api/edit-image/route.ts b/templates/next-image/src/app/api/edit-image/route.ts index eb619a5fb..11c52b38e 100644 --- a/templates/next-image/src/app/api/edit-image/route.ts +++ b/templates/next-image/src/app/api/edit-image/route.ts @@ -17,6 +17,14 @@ const providers = { gemini: handleGoogleEdit, }; +export const config = { + api: { + bodyParser: { + sizeLimit: '4mb', + }, + }, +}; + export async function POST(req: Request) { try { const body = await req.json(); diff --git a/templates/next-image/src/app/api/generate-image/route.ts b/templates/next-image/src/app/api/generate-image/route.ts index 5a3f9a8b0..15bf30c3a 100644 --- a/templates/next-image/src/app/api/generate-image/route.ts +++ b/templates/next-image/src/app/api/generate-image/route.ts @@ -19,6 +19,14 @@ const providers = { gemini: handleGoogleGenerate, }; +export const config = { + api: { + bodyParser: { + sizeLimit: '4mb', + }, + }, +}; + export async function POST(req: Request) { try { const body = await req.json(); From e7993443cf48f455f58be568055c01a7a994b3b5 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 12:45:36 -0400 Subject: [PATCH 11/50] remove openrouter/auto --- packages/sdk/ts/src/supported-models/chat/openrouter.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/sdk/ts/src/supported-models/chat/openrouter.ts b/packages/sdk/ts/src/supported-models/chat/openrouter.ts index d7321a2f8..4874b0ffe 100644 --- a/packages/sdk/ts/src/supported-models/chat/openrouter.ts +++ b/packages/sdk/ts/src/supported-models/chat/openrouter.ts @@ -240,7 +240,6 @@ export type OpenRouterModel = | 'openai/o4-mini' | 'openai/o4-mini-high' | 'opengvlab/internvl3-14b' - | 'openrouter/auto' | 'perplexity/r1-1776' | 'perplexity/sonar' | 'perplexity/sonar-deep-research' @@ -1752,12 +1751,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 4e-7, provider: 'OpenRouter', }, - { - model_id: 'openrouter/auto', - input_cost_per_token: -1, - output_cost_per_token: -1, - provider: 'OpenRouter', - }, { model_id: 'perplexity/r1-1776', input_cost_per_token: 0.000002, From 8b1b22711234c1e29201d5dbdc71c8379d45af06 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 12:47:55 -0400 Subject: [PATCH 12/50] fix model price --- packages/app/server/src/services/AccountingService.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index ba654064a..106dcea13 100644 --- a/packages/app/server/src/services/AccountingService.ts +++ b/packages/app/server/src/services/AccountingService.ts @@ -124,10 +124,19 @@ 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) { + throw new Error(`Invalid pricing for model: ${model}`); + } - return new Decimal(modelPrice.input_cost_per_token) + const cost = new Decimal(modelPrice.input_cost_per_token) .mul(inputTokens) .plus(new Decimal(modelPrice.output_cost_per_token).mul(outputTokens)); + + if (cost.lessThan(0)) { + throw new Error(`Invalid cost for model: ${model}`); + } + + return cost; }; export const getImageModelCost = ( From 0cded6c0a3d3244595ad3297a9a3d3917241aa3f Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 19:36:24 -0400 Subject: [PATCH 13/50] 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 (
Date: Tue, 14 Oct 2025 21:03:44 -0400 Subject: [PATCH 14/50] add E2b, fix tavily --- packages/app/server/package.json | 1 + packages/app/server/src/resources/e2b/e2b.ts | 63 +++++ .../app/server/src/resources/e2b/prices.ts | 3 + .../app/server/src/resources/e2b/route.ts | 58 ++++ .../app/server/src/resources/e2b/types.ts | 83 ++++++ .../app/server/src/resources/tavily/route.ts | 29 +- .../app/server/src/resources/tavily/tavily.ts | 4 +- .../app/server/src/resources/tavily/types.ts | 2 +- packages/app/server/src/routers/resource.ts | 20 +- .../app/server/src/schema/schemaForRoute.ts | 17 ++ pnpm-lock.yaml | 265 ++++++++++-------- 11 files changed, 397 insertions(+), 148 deletions(-) create mode 100644 packages/app/server/src/resources/e2b/e2b.ts create mode 100644 packages/app/server/src/resources/e2b/prices.ts create mode 100644 packages/app/server/src/resources/e2b/route.ts create mode 100644 packages/app/server/src/resources/e2b/types.ts diff --git a/packages/app/server/package.json b/packages/app/server/package.json index c44b75548..3568a93df 100644 --- a/packages/app/server/package.json +++ b/packages/app/server/package.json @@ -43,6 +43,7 @@ "dependencies": { "@coinbase/cdp-sdk": "^1.34.0", "@coinbase/x402": "^0.6.5", + "@e2b/code-interpreter": "^2.0.1", "@google-cloud/storage": "^7.17.1", "@google/genai": "^1.20.0", "@merit-systems/echo-typescript-sdk": "workspace:*", diff --git a/packages/app/server/src/resources/e2b/e2b.ts b/packages/app/server/src/resources/e2b/e2b.ts new file mode 100644 index 000000000..36ccc38a9 --- /dev/null +++ b/packages/app/server/src/resources/e2b/e2b.ts @@ -0,0 +1,63 @@ +import { Sandbox } from "@e2b/code-interpreter"; +import dotenv from 'dotenv'; +import { E2BExecuteOutput, E2BExecuteInput } from "./types"; +import { DEFAULT_VCPU_COUNT, PRICE_PER_VCPU_PER_SECOND } from "./prices"; +import { Decimal } from "@prisma/client/runtime/library"; +import { Transaction } from "types"; +dotenv.config(); + +export const calculateE2BExecuteCost = (): Decimal => { + const estimatedDurationSeconds = 10; + return new Decimal((estimatedDurationSeconds) * PRICE_PER_VCPU_PER_SECOND * DEFAULT_VCPU_COUNT); +}; + +export const createE2BTransaction = ( + input: E2BExecuteInput, + output: E2BExecuteOutput, + cost: Decimal +): Transaction => { + return { + metadata: { + providerId: output.sandboxId, + provider: 'e2b', + model: 'sandbox', + inputTokens: output.duration, + outputTokens: output.duration, + totalTokens: output.duration, + toolCost: cost, + }, + rawTransactionCost: cost, + status: 'completed', + }; +}; + +export const e2bExecutePythonSnippet = async (snippet: string): Promise => { + if (!process.env.E2B_API_KEY) { + throw new Error( + "E2B_API_KEY environment variable is required but not set", + ); + } + + const startTime = performance.now(); + const sandbox = await Sandbox.create({ + apiKey: process.env.E2B_API_KEY, + }); + const { results, logs, error, executionCount } = await sandbox.runCode(snippet, { + timeoutMs: 10000, + requestTimeoutMs: 15000, + }); + await sandbox.kill(); + const endTime = performance.now(); + const durationMs = endTime - startTime; + const duration = durationMs / 1000; + const cost = duration * PRICE_PER_VCPU_PER_SECOND * DEFAULT_VCPU_COUNT; + return { + results: results, + logs: logs, + error: error, + executionCount: executionCount, + cost: cost, + sandboxId: sandbox.sandboxId, + duration: duration, + }; +}; \ No newline at end of file diff --git a/packages/app/server/src/resources/e2b/prices.ts b/packages/app/server/src/resources/e2b/prices.ts new file mode 100644 index 000000000..f57fe4b67 --- /dev/null +++ b/packages/app/server/src/resources/e2b/prices.ts @@ -0,0 +1,3 @@ +export const PRICE_PER_VCPU_PER_SECOND = 0.000014; + +export const DEFAULT_VCPU_COUNT = 2; \ No newline at end of file diff --git a/packages/app/server/src/resources/e2b/route.ts b/packages/app/server/src/resources/e2b/route.ts new file mode 100644 index 000000000..eff51706c --- /dev/null +++ b/packages/app/server/src/resources/e2b/route.ts @@ -0,0 +1,58 @@ +import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { E2BExecuteInputSchema } from './types'; +import { calculateE2BExecuteCost, e2bExecutePythonSnippet, createE2BTransaction } from './e2b'; +import { authenticateRequest } from 'auth'; +import { prisma } from 'server'; +import { settle } from 'handlers'; +import { finalize } from 'handlers'; +import { Decimal } from '@prisma/client/runtime/library'; +import { Request, Response } from 'express'; + +export async function e2bExecuteRoute(req: Request, res: Response) { + const headers = req.headers as Record; + + const maxCost = calculateE2BExecuteCost(); + + if (!isApiRequest(headers) && !isX402Request(headers)) { + return buildX402Response(req, res, maxCost); + } + + const inputBody = E2BExecuteInputSchema.safeParse(req.body); + if (!inputBody.success) { + return res.status(400).json({ error: 'Invalid body', issues: inputBody.error.issues }); + } + const parsedBody = inputBody.data; + + if (isApiRequest(headers)) { + const { echoControlService } = await authenticateRequest(headers, prisma); + + const output = await e2bExecutePythonSnippet(parsedBody.snippet); + + const actualCost = new Decimal(output.cost); + const transaction = createE2BTransaction(parsedBody, output, actualCost); + + await echoControlService.createTransaction(transaction, actualCost); + + 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 e2bExecutePythonSnippet(parsedBody.snippet); + + const actualCost = new Decimal(output.cost); + const transaction = createE2BTransaction(parsedBody, output, actualCost); + + finalize(paymentAmountDecimal, transaction, payload).catch((error) => { + console.error('Failed to finalize transaction:', error); + }); + + return res.status(200).json(output); + } else { + return buildX402Response(req, res, maxCost); + } +} + diff --git a/packages/app/server/src/resources/e2b/types.ts b/packages/app/server/src/resources/e2b/types.ts new file mode 100644 index 000000000..8b96cc5f3 --- /dev/null +++ b/packages/app/server/src/resources/e2b/types.ts @@ -0,0 +1,83 @@ +import { z } from "zod"; + +// Input schema +export const E2BExecuteInputSchema = z.object({ + snippet: z.string().min(1, "Code snippet cannot be empty"), +}); + +export type E2BExecuteInput = z.infer; + +// Chart type enums +export const ChartTypeSchema = z.enum([ + "line", + "scatter", + "bar", + "pie", + "box_and_whisker", + "superchart", + "unknown", +]); + +export const ScaleTypeSchema = z.enum([ + "linear", + "datetime", + "categorical", + "log", + "symlog", + "logit", + "function", + "functionlog", + "asinh", +]); + +// Result schema +export const ResultSchema = z.object({ + isMainResult: z.boolean(), + text: z.string().optional(), + html: z.string().optional(), + markdown: z.string().optional(), + svg: z.string().optional(), + png: z.string().optional(), + jpeg: z.string().optional(), + pdf: z.string().optional(), + latex: z.string().optional(), + json: z.string().optional(), + javascript: z.string().optional(), + data: z.record(z.string(), z.unknown()).optional(), + chart: z.unknown().optional(), // ChartTypes is complex + extra: z.unknown().optional(), + raw: z.record(z.string(), z.unknown()), +}); + +export type Result = z.infer; + +// Logs schema +export const LogsSchema = z.object({ + stdout: z.array(z.string()), + stderr: z.array(z.string()), +}); + +export type Logs = z.infer; + +// Execution error schema +export const ExecutionErrorSchema = z.object({ + name: z.string(), + value: z.string(), + traceback: z.string(), +}); + +export type ExecutionError = z.infer; + +// Output schema +export const E2BExecuteOutputSchema = z.object({ + results: z.array(ResultSchema), + logs: LogsSchema, + error: ExecutionErrorSchema.optional(), + executionCount: z.number().optional(), + cost: z.number(), + sandboxId: z.string(), + duration: z.number(), +}); + +export type E2BExecuteOutput = z.infer; + diff --git a/packages/app/server/src/resources/tavily/route.ts b/packages/app/server/src/resources/tavily/route.ts index b8f9c6852..1db9e2fe5 100644 --- a/packages/app/server/src/resources/tavily/route.ts +++ b/packages/app/server/src/resources/tavily/route.ts @@ -13,40 +13,47 @@ 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); + const inputBody = TavilySearchInputSchema.safeParse(req.body); + const maxCost = calculateTavilySearchCost(inputBody.data); + if (!isApiRequest(headers) && !isX402Request(headers)) { - return buildX402Response(req, res, maxCost); + return buildX402Response(req, res, maxCost); } + if (!inputBody.success) { + return res.status(400).json({ error: 'Invalid body', issues: inputBody.error.issues }); + } + + const parsedBody = inputBody.data; + const safeMaxCost = calculateTavilySearchCost(parsedBody); + if (isApiRequest(headers)) { const { echoControlService } = await authenticateRequest(headers, prisma); - const output = await tavilySearch(inputBody); + const output = await tavilySearch(parsedBody); - const transaction = createTavilyTransaction(inputBody, output, maxCost); + const transaction = createTavilyTransaction(parsedBody, output, safeMaxCost); - await echoControlService.createTransaction(transaction, maxCost); + await echoControlService.createTransaction(transaction, safeMaxCost); return res.status(200).json(output); } else if (isX402Request(headers)) { - const settleResult = await settle(req, res, headers, maxCost); + const settleResult = await settle(req, res, headers, safeMaxCost); if (!settleResult) { return buildX402Response(req, res, maxCost); } const { payload, paymentAmountDecimal } = settleResult; - const output = await tavilySearch(inputBody); + const output = await tavilySearch(parsedBody); - const transaction = createTavilyTransaction(inputBody, output, maxCost); + const transaction = createTavilyTransaction(parsedBody, output, safeMaxCost); await finalize(paymentAmountDecimal, transaction, payload); return res.status(200).json(output); } else { - return buildX402Response(req, res, maxCost); + return buildX402Response(req, res, safeMaxCost); } } catch (error) { logger.error('Error searching tavily', error); diff --git a/packages/app/server/src/resources/tavily/tavily.ts b/packages/app/server/src/resources/tavily/tavily.ts index 808797f72..7f6d61eed 100644 --- a/packages/app/server/src/resources/tavily/tavily.ts +++ b/packages/app/server/src/resources/tavily/tavily.ts @@ -8,9 +8,9 @@ import { import { Transaction } from 'types'; export const calculateTavilySearchCost = ( - input: TavilySearchInput + input: TavilySearchInput | undefined ): Decimal => { - const price = TAVILY_SEARCH_PRICING[input.search_depth ?? 'basic']; + const price = TAVILY_SEARCH_PRICING[input?.search_depth ?? 'basic']; return new Decimal(price).mul(CREDIT_PRICE); }; diff --git a/packages/app/server/src/resources/tavily/types.ts b/packages/app/server/src/resources/tavily/types.ts index 8f4deacf5..91ab38695 100644 --- a/packages/app/server/src/resources/tavily/types.ts +++ b/packages/app/server/src/resources/tavily/types.ts @@ -36,7 +36,7 @@ export const TavilySearchResultSchema = z.object({ export const TavilySearchOutputSchema = z.object({ query: z.string(), - answer: z.string().optional(), + answer: z.string().nullish(), images: z.array(z.string()), results: z.array(TavilySearchResultSchema), auto_parameters: z diff --git a/packages/app/server/src/routers/resource.ts b/packages/app/server/src/routers/resource.ts index 038851927..05410907e 100644 --- a/packages/app/server/src/routers/resource.ts +++ b/packages/app/server/src/routers/resource.ts @@ -1,21 +1,15 @@ -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 { buildX402Response, isApiRequest, isX402Request } from 'utils'; -import { authenticateRequest } from 'auth'; -import { prisma } from 'server'; -import { settle, finalize } from 'handlers'; +import { Request, Response, Router } from 'express'; import { tavilySearchRoute } from '../resources/tavily/route'; +import { e2bExecuteRoute } from '../resources/e2b/route'; + const resourceRouter: Router = Router(); resourceRouter.post('/tavily/search', async (req: Request, res: Response) => { return await tavilySearchRoute(req, res); }); +resourceRouter.post('/e2b/execute', async (req: Request, res: Response) => { + return await e2bExecuteRoute(req, res); +}); + export default resourceRouter; diff --git a/packages/app/server/src/schema/schemaForRoute.ts b/packages/app/server/src/schema/schemaForRoute.ts index 7e0fe9bf8..b7ec45a80 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -13,6 +13,7 @@ import { TavilySearchInputSchema, TavilySearchOutputSchema, } from 'resources/tavily/types'; +import { E2BExecuteInputSchema, E2BExecuteOutputSchema } from 'resources/e2b/types'; export function getSchemaForRoute(path: string): | { @@ -100,6 +101,22 @@ export function getSchemaForRoute(path: string): output: outputSchema.properties, }; } + if (path.endsWith('/e2b/execute')) { + const inputSchema = z.toJSONSchema(E2BExecuteInputSchema, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(E2BExecuteOutputSchema, { + target: 'openapi-3.0', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } return undefined; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 013382483..48b839a48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 2.10.0(@prisma/client@6.16.0(prisma@6.16.0(magicast@0.3.5)(typescript@5.9.2))(typescript@5.9.2)) '@coinbase/x402': specifier: ^0.6.4 - version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) '@hookform/resolvers': specifier: ^5.2.1 version: 5.2.1(react-hook-form@7.62.0(react@19.1.1)) @@ -419,7 +419,10 @@ importers: version: 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@coinbase/x402': specifier: ^0.6.5 - version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@e2b/code-interpreter': + specifier: ^2.0.1 + version: 2.0.1 '@google-cloud/storage': specifier: ^7.17.1 version: 7.17.1 @@ -506,7 +509,7 @@ importers: version: 2.7.0 openai: specifier: ^6.2.0 - version: 6.2.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11) + version: 6.2.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11) prisma: specifier: 6.16.0 version: 6.16.0(magicast@0.3.5)(typescript@5.9.2) @@ -530,7 +533,7 @@ importers: version: 3.17.0 x402-express: specifier: ^0.6.5 - version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^4.1.11 version: 4.1.11 @@ -1687,6 +1690,9 @@ packages: '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + '@bufbuild/protobuf@2.9.0': + resolution: {integrity: sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==} + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -1730,6 +1736,17 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@connectrpc/connect-web@2.0.0-rc.3': + resolution: {integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0-rc.3 + + '@connectrpc/connect@2.0.0-rc.3': + resolution: {integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1772,6 +1789,10 @@ packages: resolution: {integrity: sha512-LQ8cem3RU/mI2iz5Sy+ypnhfhVge3bc9tsLJg5rdf7j7u1RRTfmmSdLwSjeYI7sL9ToN7rgFkOGSBJqaBT+gSQ==} hasBin: true + '@e2b/code-interpreter@2.0.1': + resolution: {integrity: sha512-CbH5tU5lI0QyO+vTOdW9mZGpuVImmH96SsRqEduplrv7hplINMsvoQESvcDavdJWjurK3Oe/Ho8QtpDb1bBKag==} + engines: {node: '>=20'} + '@ecies/ciphers@0.2.4': resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -6975,6 +6996,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dockerfile-ast@0.7.1: + resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -7035,6 +7059,10 @@ packages: duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + e2b@2.2.10: + resolution: {integrity: sha512-VjdtAzT7JCkawrmVs2aJOrIWvMvUpI7aAw9wqXUfZWD8vpsW2rvKvPGxAwdbA+KOe5wkSwoRmS6TMICKvIfVqA==} + engines: {node: '>=20'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -7893,6 +7921,11 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -8458,6 +8491,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + jayson@4.2.0: resolution: {integrity: sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==} engines: {node: '>=8'} @@ -9553,6 +9590,9 @@ packages: openapi-fetch@0.13.8: resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + openapi-fetch@0.14.1: + resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==} + openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} @@ -9685,6 +9725,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -9777,6 +9821,9 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + playwright-core@1.53.0: resolution: {integrity: sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==} engines: {node: '>=18'} @@ -12427,6 +12474,8 @@ snapshots: '@braintree/sanitize-url@7.1.1': {} + '@bufbuild/protobuf@2.9.0': {} + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -12577,11 +12626,11 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -12617,11 +12666,11 @@ snapshots: - utf-8-validate - ws - '@coinbase/x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -12659,6 +12708,15 @@ snapshots: '@colors/colors@1.6.0': {} + '@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.9.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.9.0))': + dependencies: + '@bufbuild/protobuf': 2.9.0 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.9.0) + + '@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.9.0)': + dependencies: + '@bufbuild/protobuf': 2.9.0 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -12703,6 +12761,10 @@ snapshots: picomatch: 4.0.3 which: 4.0.0 + '@e2b/code-interpreter@2.0.1': + dependencies: + e2b: 2.2.10 + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': dependencies: '@noble/ciphers': 1.3.0 @@ -17330,10 +17392,6 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -17342,13 +17400,9 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.2))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(typescript@5.9.2) '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: @@ -17360,13 +17414,9 @@ snapshots: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(typescript@5.9.2))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(typescript@5.9.2) '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: @@ -17376,9 +17426,9 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.5.1(@solana/kit@2.3.0(typescript@5.9.2))': dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(typescript@5.9.2) '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: @@ -17535,31 +17585,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17610,7 +17635,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@2.3.0(typescript@5.9.2)': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17623,11 +17648,11 @@ snapshots: '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 @@ -17717,15 +17742,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -17752,24 +17768,6 @@ snapshots: '@solana/subscribable': 2.3.0(typescript@5.9.2) typescript: 5.9.2 - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -17912,23 +17910,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17963,7 +17944,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@2.3.0(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17971,7 +17952,7 @@ snapshots: '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/promises': 2.3.0(typescript@5.9.2) '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -19047,14 +19028,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.11.2(@types/node@20.19.16)(typescript@5.9.2) - vite: 6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: @@ -19102,7 +19083,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.3)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.19.16)(@vitest/ui@3.2.3)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/utils@3.0.9': dependencies: @@ -21582,6 +21563,11 @@ snapshots: dlv@1.1.3: {} + dockerfile-ast@0.7.1: + dependencies: + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -21649,6 +21635,19 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 + e2b@2.2.10: + dependencies: + '@bufbuild/protobuf': 2.9.0 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.9.0) + '@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.9.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.9.0)) + chalk: 5.4.1 + compare-versions: 6.1.1 + dockerfile-ast: 0.7.1 + glob: 11.0.3 + openapi-fetch: 0.14.1 + platform: 1.3.6 + tar: 7.4.3 + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -22822,6 +22821,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + globals@11.12.0: {} globals@14.0.0: {} @@ -23470,6 +23478,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + jayson@4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@types/connect': 3.4.38 @@ -24914,15 +24926,19 @@ snapshots: ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.11 - openai@6.2.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11): + openai@6.2.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11): optionalDependencies: - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.11 openapi-fetch@0.13.8: dependencies: openapi-typescript-helpers: 0.0.15 + openapi-fetch@0.14.1: + dependencies: + openapi-typescript-helpers: 0.0.15 + openapi-typescript-helpers@0.0.15: {} optionator@0.9.4: @@ -25176,6 +25192,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.2.1 + minipass: 7.1.2 + path-to-regexp@0.1.12: {} path-to-regexp@6.3.0: {} @@ -25265,6 +25286,8 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + platform@1.3.6: {} + playwright-core@1.53.0: {} playwright@1.53.0: @@ -27730,7 +27753,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 @@ -28185,13 +28208,13 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402-express@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-express@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) express: 4.21.2 viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -28434,14 +28457,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(typescript@5.9.2)) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(typescript@5.9.2)) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(typescript@5.9.2)) + '@solana/kit': 2.3.0(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 @@ -28478,14 +28501,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 From 2ac034822e76feab04e88359ce98ec002fec116b Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 21:04:06 -0400 Subject: [PATCH 15/50] format --- packages/app/server/src/schema/schemaForRoute.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/server/src/schema/schemaForRoute.ts b/packages/app/server/src/schema/schemaForRoute.ts index b7ec45a80..b3571821e 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -13,7 +13,10 @@ import { TavilySearchInputSchema, TavilySearchOutputSchema, } from 'resources/tavily/types'; -import { E2BExecuteInputSchema, E2BExecuteOutputSchema } from 'resources/e2b/types'; +import { + E2BExecuteInputSchema, + E2BExecuteOutputSchema, +} from 'resources/e2b/types'; export function getSchemaForRoute(path: string): | { From 9b98dc1c10286cfa5c0cea7a97b46b71d44091e1 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 21:42:41 -0400 Subject: [PATCH 16/50] add long tail tavily routes --- packages/app/server/src/handlers.ts | 4 +- packages/app/server/src/resources/e2b/e2b.ts | 2 +- .../src/resources/tavily/crawl/route.ts | 65 +++++++++++ .../src/resources/tavily/crawl/tavily.ts | 102 ++++++++++++++++++ .../src/resources/tavily/crawl/types.ts | 38 +++++++ .../src/resources/tavily/extract/route.ts | 66 ++++++++++++ .../src/resources/tavily/extract/tavily.ts | 83 ++++++++++++++ .../src/resources/tavily/extract/types.ts | 36 +++++++ .../app/server/src/resources/tavily/prices.ts | 42 +------- .../resources/tavily/{ => search}/route.ts | 0 .../resources/tavily/{ => search}/tavily.ts | 4 +- .../resources/tavily/{ => search}/types.ts | 18 ++-- packages/app/server/src/routers/resource.ts | 12 ++- .../app/server/src/schema/schemaForRoute.ts | 42 +++++++- 14 files changed, 458 insertions(+), 56 deletions(-) create mode 100644 packages/app/server/src/resources/tavily/crawl/route.ts create mode 100644 packages/app/server/src/resources/tavily/crawl/tavily.ts create mode 100644 packages/app/server/src/resources/tavily/crawl/types.ts create mode 100644 packages/app/server/src/resources/tavily/extract/route.ts create mode 100644 packages/app/server/src/resources/tavily/extract/tavily.ts create mode 100644 packages/app/server/src/resources/tavily/extract/types.ts rename packages/app/server/src/resources/tavily/{ => search}/route.ts (100%) rename packages/app/server/src/resources/tavily/{ => search}/tavily.ts (92%) rename packages/app/server/src/resources/tavily/{ => search}/types.ts (71%) diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 49d641e21..d45194496 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -156,7 +156,9 @@ export async function handleX402Request({ } catch (error) { const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); const authPayload = payload.authorization; - await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); + await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt).catch((error) => { + console.error('Failed to transfer refund amount:', error); + }); } } diff --git a/packages/app/server/src/resources/e2b/e2b.ts b/packages/app/server/src/resources/e2b/e2b.ts index 36ccc38a9..0467b0afc 100644 --- a/packages/app/server/src/resources/e2b/e2b.ts +++ b/packages/app/server/src/resources/e2b/e2b.ts @@ -3,7 +3,7 @@ import dotenv from 'dotenv'; import { E2BExecuteOutput, E2BExecuteInput } from "./types"; import { DEFAULT_VCPU_COUNT, PRICE_PER_VCPU_PER_SECOND } from "./prices"; import { Decimal } from "@prisma/client/runtime/library"; -import { Transaction } from "types"; +import { Transaction } from "../../types"; dotenv.config(); export const calculateE2BExecuteCost = (): Decimal => { diff --git a/packages/app/server/src/resources/tavily/crawl/route.ts b/packages/app/server/src/resources/tavily/crawl/route.ts new file mode 100644 index 000000000..a5574dc1b --- /dev/null +++ b/packages/app/server/src/resources/tavily/crawl/route.ts @@ -0,0 +1,65 @@ +import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { TavilyCrawlInputSchema } from './types'; +import { + calculateTavilyCrawlMaxCost, + calculateTavilyCrawlActualCost, + tavilyCrawl, +} from './tavily'; +import { authenticateRequest } from 'auth'; +import { prisma } from 'server'; +import { settle } from 'handlers'; +import { finalize } from 'handlers'; +import { createTavilyTransaction } from './tavily'; +import { Request, Response } from 'express'; + +export async function tavilyCrawlRoute(req: Request, res: Response) { + const headers = req.headers as Record; + + const inputBody = TavilyCrawlInputSchema.safeParse(req.body); + + const maxCost = calculateTavilyCrawlMaxCost(inputBody.data); + + if (!isApiRequest(headers) && !isX402Request(headers)) { + return buildX402Response(req, res, maxCost); + } + + if (!inputBody.success) { + return res + .status(400) + .json({ error: 'Invalid body', issues: inputBody.error.issues }); + } + + const parsedBody = inputBody.data; + const safeMaxCost = calculateTavilyCrawlMaxCost(parsedBody); + + if (isApiRequest(headers)) { + const { echoControlService } = await authenticateRequest(headers, prisma); + + const output = await tavilyCrawl(parsedBody); + + const actualCost = calculateTavilyCrawlActualCost(parsedBody, output); + const transaction = createTavilyTransaction(parsedBody, output, actualCost); + + await echoControlService.createTransaction(transaction, actualCost); + + return res.status(200).json(output); + } else if (isX402Request(headers)) { + const settleResult = await settle(req, res, headers, safeMaxCost); + if (!settleResult) { + return buildX402Response(req, res, maxCost); + } + const { payload, paymentAmountDecimal } = settleResult; + + const output = await tavilyCrawl(parsedBody); + + const actualCost = calculateTavilyCrawlActualCost(parsedBody, output); + const transaction = createTavilyTransaction(parsedBody, output, actualCost); + + await finalize(paymentAmountDecimal, transaction, payload); + + return res.status(200).json(output); + } else { + return buildX402Response(req, res, safeMaxCost); + } +} + diff --git a/packages/app/server/src/resources/tavily/crawl/tavily.ts b/packages/app/server/src/resources/tavily/crawl/tavily.ts new file mode 100644 index 000000000..0cfedefd3 --- /dev/null +++ b/packages/app/server/src/resources/tavily/crawl/tavily.ts @@ -0,0 +1,102 @@ +import { Decimal } from '@prisma/client/runtime/library'; +import { CREDIT_PRICE, TAVILY_MAP_PRICING, TAVILY_EXTRACT_PRICING } from '../prices'; +import { + TavilyCrawlInput, + TavilyCrawlOutput, + TavilyCrawlOutputSchema, +} from './types'; +import { Transaction } from '../../../types'; + +export const calculateTavilyCrawlMaxCost = ( + input: TavilyCrawlInput | undefined +): Decimal => { + if (!input) { + return new Decimal(0); + } + + // Max cost is based on the limit parameter (default 50) + const maxPages = input.limit ?? 50; + const hasInstructions = !!input.instructions; + const extractDepth = input.extract_depth ?? 'basic'; + + // Mapping cost + const mapPricing = hasInstructions + ? TAVILY_MAP_PRICING.withInstructions + : TAVILY_MAP_PRICING.regular; + const mapCredits = Math.ceil(maxPages / mapPricing.pagesPerCredit) * mapPricing.creditsPerUnit; + + // Extraction cost + const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[extractDepth]; + const extractCredits = Math.ceil(maxPages / urlsPerCredit) * creditsPerUnit; + + const totalCredits = mapCredits + extractCredits; + return new Decimal(totalCredits).mul(CREDIT_PRICE); +}; + +export const calculateTavilyCrawlActualCost = ( + input: TavilyCrawlInput, + output: TavilyCrawlOutput +): Decimal => { + const successfulPages = output.results.length; + const hasInstructions = !!input.instructions; + const extractDepth = input.extract_depth ?? 'basic'; + + // Mapping cost + const mapPricing = hasInstructions + ? TAVILY_MAP_PRICING.withInstructions + : TAVILY_MAP_PRICING.regular; + const mapCredits = Math.ceil(successfulPages / mapPricing.pagesPerCredit) * mapPricing.creditsPerUnit; + + // Extraction cost + const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[extractDepth]; + const extractCredits = Math.ceil(successfulPages / urlsPerCredit) * creditsPerUnit; + + const totalCredits = mapCredits + extractCredits; + return new Decimal(totalCredits).mul(CREDIT_PRICE); +}; + +export const createTavilyTransaction = ( + input: TavilyCrawlInput, + output: TavilyCrawlOutput, + cost: Decimal +): Transaction => { + return { + metadata: { + providerId: output.request_id, + provider: 'tavily', + model: `crawl-${input.extract_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 tavilyCrawl( + input: TavilyCrawlInput +): Promise { + const response = await fetch('https://api.tavily.com/crawl', { + 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 TavilyCrawlOutputSchema.parse(data); +} + diff --git a/packages/app/server/src/resources/tavily/crawl/types.ts b/packages/app/server/src/resources/tavily/crawl/types.ts new file mode 100644 index 000000000..d091aae0d --- /dev/null +++ b/packages/app/server/src/resources/tavily/crawl/types.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; + +// Input schema +export const TavilyCrawlInputSchema = z.object({ + url: z.string(), + instructions: z.string().optional(), + max_depth: z.coerce.number().int().min(1).optional(), + max_breadth: z.coerce.number().int().min(1).optional(), + limit: z.coerce.number().int().min(1).optional(), + select_paths: z.array(z.string()).nullable().optional(), + select_domains: z.array(z.string()).nullable().optional(), + exclude_paths: z.array(z.string()).nullable().optional(), + exclude_domains: z.array(z.string()).nullable().optional(), + allow_external: z.coerce.boolean().optional(), + include_images: z.coerce.boolean().optional(), + extract_depth: z.enum(['basic', 'advanced']).optional(), + format: z.enum(['markdown', 'text']).optional(), + include_favicon: z.coerce.boolean().optional(), +}); + +export type TavilyCrawlInput = z.infer; + +// Output schema +export const TavilyCrawlResultSchema = z.object({ + url: z.string(), + raw_content: z.string().nullable(), + favicon: z.string().optional(), +}); + +export const TavilyCrawlOutputSchema = z.object({ + base_url: z.string(), + results: z.array(TavilyCrawlResultSchema), + response_time: z.number(), + request_id: z.string(), +}); + +export type TavilyCrawlOutput = z.infer; + diff --git a/packages/app/server/src/resources/tavily/extract/route.ts b/packages/app/server/src/resources/tavily/extract/route.ts new file mode 100644 index 000000000..57d496a0f --- /dev/null +++ b/packages/app/server/src/resources/tavily/extract/route.ts @@ -0,0 +1,66 @@ +import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { TavilyExtractInputSchema } from './types'; +import { + calculateTavilyExtractMaxCost, + calculateTavilyExtractActualCost, + tavilyExtract, +} 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 tavilyExtractRoute(req: Request, res: Response) { + const headers = req.headers as Record; + + const inputBody = TavilyExtractInputSchema.safeParse(req.body); + + const maxCost = calculateTavilyExtractMaxCost(inputBody.data); + + if (!isApiRequest(headers) && !isX402Request(headers)) { + return buildX402Response(req, res, maxCost); + } + + if (!inputBody.success) { + return res + .status(400) + .json({ error: 'Invalid body', issues: inputBody.error.issues }); + } + + const parsedBody = inputBody.data; + const safeMaxCost = calculateTavilyExtractMaxCost(parsedBody); + + if (isApiRequest(headers)) { + const { echoControlService } = await authenticateRequest(headers, prisma); + + const output = await tavilyExtract(parsedBody); + + const actualCost = calculateTavilyExtractActualCost(parsedBody, output); + const transaction = createTavilyTransaction(parsedBody, output, actualCost); + + await echoControlService.createTransaction(transaction, actualCost); + + return res.status(200).json(output); + } else if (isX402Request(headers)) { + const settleResult = await settle(req, res, headers, safeMaxCost); + if (!settleResult) { + return buildX402Response(req, res, maxCost); + } + const { payload, paymentAmountDecimal } = settleResult; + + const output = await tavilyExtract(parsedBody); + + const actualCost = calculateTavilyExtractActualCost(parsedBody, output); + const transaction = createTavilyTransaction(parsedBody, output, actualCost); + + await finalize(paymentAmountDecimal, transaction, payload); + + return res.status(200).json(output); + } else { + return buildX402Response(req, res, safeMaxCost); + } +} + diff --git a/packages/app/server/src/resources/tavily/extract/tavily.ts b/packages/app/server/src/resources/tavily/extract/tavily.ts new file mode 100644 index 000000000..8b8ef1822 --- /dev/null +++ b/packages/app/server/src/resources/tavily/extract/tavily.ts @@ -0,0 +1,83 @@ +import { Decimal } from '@prisma/client/runtime/library'; +import { CREDIT_PRICE, TAVILY_EXTRACT_PRICING } from '../prices'; +import { + TavilyExtractInput, + TavilyExtractOutput, + TavilyExtractOutputSchema, +} from './types'; +import { Transaction } from '../../../types'; + +export const calculateTavilyExtractMaxCost = ( + input: TavilyExtractInput | undefined +): Decimal => { + if (!input) { + return new Decimal(0); + } + + const urlCount = Array.isArray(input.urls) ? input.urls.length : 1; + const depth = input.extract_depth ?? 'basic'; + const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[depth]; + + // Calculate max cost assuming all URLs succeed + const credits = Math.ceil(urlCount / urlsPerCredit) * creditsPerUnit; + return new Decimal(credits).mul(CREDIT_PRICE); +}; + +export const calculateTavilyExtractActualCost = ( + input: TavilyExtractInput, + output: TavilyExtractOutput +): Decimal => { + const successfulUrlCount = output.results.length; + const depth = input.extract_depth ?? 'basic'; + const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[depth]; + + // Calculate actual cost based on successful URLs + const credits = Math.ceil(successfulUrlCount / urlsPerCredit) * creditsPerUnit; + return new Decimal(credits).mul(CREDIT_PRICE); +}; + +export const createTavilyTransaction = ( + input: TavilyExtractInput, + output: TavilyExtractOutput, + cost: Decimal +): Transaction => { + return { + metadata: { + providerId: output.request_id, + provider: 'tavily', + model: `extract-${input.extract_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 tavilyExtract( + input: TavilyExtractInput +): Promise { + const response = await fetch('https://api.tavily.com/extract', { + 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 TavilyExtractOutputSchema.parse(data); +} + diff --git a/packages/app/server/src/resources/tavily/extract/types.ts b/packages/app/server/src/resources/tavily/extract/types.ts new file mode 100644 index 000000000..aa7358000 --- /dev/null +++ b/packages/app/server/src/resources/tavily/extract/types.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +// Input schema +export const TavilyExtractInputSchema = z.object({ + urls: z.union([z.string(), z.array(z.string())]), + include_images: z.coerce.boolean().optional(), + include_favicon: z.coerce.boolean().optional(), + extract_depth: z.enum(['basic', 'advanced']).optional(), + format: z.enum(['markdown', 'text']).optional(), + timeout: z.coerce.number().min(1).max(60).optional(), +}); + +export type TavilyExtractInput = z.infer; + +// Output schema +export const TavilyExtractResultSchema = z.object({ + url: z.string(), + raw_content: z.string(), + images: z.array(z.string()).optional(), + favicon: z.string().optional(), +}); + +export const TavilyExtractFailedResultSchema = z.object({ + url: z.string(), + error: z.string(), +}); + +export const TavilyExtractOutputSchema = z.object({ + results: z.array(TavilyExtractResultSchema), + failed_results: z.array(TavilyExtractFailedResultSchema), + response_time: z.number(), + request_id: z.string(), +}); + +export type TavilyExtractOutput = z.infer; + diff --git a/packages/app/server/src/resources/tavily/prices.ts b/packages/app/server/src/resources/tavily/prices.ts index ced0214e3..e27e12f6b 100644 --- a/packages/app/server/src/resources/tavily/prices.ts +++ b/packages/app/server/src/resources/tavily/prices.ts @@ -28,44 +28,4 @@ export const TAVILY_MAP_PRICING = { 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; -} +} as const; \ No newline at end of file diff --git a/packages/app/server/src/resources/tavily/route.ts b/packages/app/server/src/resources/tavily/search/route.ts similarity index 100% rename from packages/app/server/src/resources/tavily/route.ts rename to packages/app/server/src/resources/tavily/search/route.ts diff --git a/packages/app/server/src/resources/tavily/tavily.ts b/packages/app/server/src/resources/tavily/search/tavily.ts similarity index 92% rename from packages/app/server/src/resources/tavily/tavily.ts rename to packages/app/server/src/resources/tavily/search/tavily.ts index 7f6d61eed..cc56b9da9 100644 --- a/packages/app/server/src/resources/tavily/tavily.ts +++ b/packages/app/server/src/resources/tavily/search/tavily.ts @@ -1,11 +1,11 @@ import { Decimal } from '@prisma/client/runtime/library'; -import { CREDIT_PRICE, TAVILY_SEARCH_PRICING } from './prices'; +import { CREDIT_PRICE, TAVILY_SEARCH_PRICING } from '../prices'; import { TavilySearchInput, TavilySearchOutput, TavilySearchOutputSchema, } from './types'; -import { Transaction } from 'types'; +import { Transaction } from '../../../types'; export const calculateTavilySearchCost = ( input: TavilySearchInput | undefined diff --git a/packages/app/server/src/resources/tavily/types.ts b/packages/app/server/src/resources/tavily/search/types.ts similarity index 71% rename from packages/app/server/src/resources/tavily/types.ts rename to packages/app/server/src/resources/tavily/search/types.ts index 91ab38695..33f7107ba 100644 --- a/packages/app/server/src/resources/tavily/types.ts +++ b/packages/app/server/src/resources/tavily/search/types.ts @@ -3,20 +3,20 @@ import { z } from 'zod'; // Input schema export const TavilySearchInputSchema = z.object({ query: z.string(), - auto_parameters: z.boolean().optional(), + auto_parameters: z.coerce.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(), + chunks_per_source: z.coerce.number().int().positive().optional(), + max_results: z.coerce.number().int().positive().optional(), time_range: z.string().nullable().optional(), - days: z.number().int().positive().optional(), + days: z.coerce.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_answer: z.coerce.boolean().optional(), + include_raw_content: z.coerce.boolean().optional(), + include_images: z.coerce.boolean().optional(), + include_image_descriptions: z.coerce.boolean().optional(), + include_favicon: z.coerce.boolean().optional(), include_domains: z.array(z.string()).optional(), exclude_domains: z.array(z.string()).optional(), country: z.string().nullable().optional(), diff --git a/packages/app/server/src/routers/resource.ts b/packages/app/server/src/routers/resource.ts index 05410907e..48fdb05d6 100644 --- a/packages/app/server/src/routers/resource.ts +++ b/packages/app/server/src/routers/resource.ts @@ -1,5 +1,7 @@ import { Request, Response, Router } from 'express'; -import { tavilySearchRoute } from '../resources/tavily/route'; +import { tavilySearchRoute } from '../resources/tavily/search/route'; +import { tavilyExtractRoute } from '../resources/tavily/extract/route'; +import { tavilyCrawlRoute } from '../resources/tavily/crawl/route'; import { e2bExecuteRoute } from '../resources/e2b/route'; const resourceRouter: Router = Router(); @@ -8,6 +10,14 @@ resourceRouter.post('/tavily/search', async (req: Request, res: Response) => { return await tavilySearchRoute(req, res); }); +resourceRouter.post('/tavily/extract', async (req: Request, res: Response) => { + return await tavilyExtractRoute(req, res); +}); + +resourceRouter.post('/tavily/crawl', async (req: Request, res: Response) => { + return await tavilyCrawlRoute(req, res); +}); + resourceRouter.post('/e2b/execute', async (req: Request, res: Response) => { return await e2bExecuteRoute(req, res); }); diff --git a/packages/app/server/src/schema/schemaForRoute.ts b/packages/app/server/src/schema/schemaForRoute.ts index b3571821e..fa44b51b9 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -12,7 +12,15 @@ import { CreateImagesRequest, CreateImagesResponse } from './image/openai'; import { TavilySearchInputSchema, TavilySearchOutputSchema, -} from 'resources/tavily/types'; +} from 'resources/tavily/search/types'; +import { + TavilyExtractInputSchema, + TavilyExtractOutputSchema, +} from 'resources/tavily/extract/types'; +import { + TavilyCrawlInputSchema, + TavilyCrawlOutputSchema, +} from 'resources/tavily/crawl/types'; import { E2BExecuteInputSchema, E2BExecuteOutputSchema, @@ -104,6 +112,38 @@ export function getSchemaForRoute(path: string): output: outputSchema.properties, }; } + if (path.endsWith('/tavily/extract')) { + const inputSchema = z.toJSONSchema(TavilyExtractInputSchema, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(TavilyExtractOutputSchema, { + target: 'openapi-3.0', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } + if (path.endsWith('/tavily/crawl')) { + const inputSchema = z.toJSONSchema(TavilyCrawlInputSchema, { + target: 'openapi-3.0', + }); + const outputSchema = z.toJSONSchema(TavilyCrawlOutputSchema, { + target: 'openapi-3.0', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + output: outputSchema.properties, + }; + } if (path.endsWith('/e2b/execute')) { const inputSchema = z.toJSONSchema(E2BExecuteInputSchema, { target: 'openapi-3.0', From 6facf82736fbd10ffe905cf9060c89753c8873e1 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 14 Oct 2025 22:03:32 -0400 Subject: [PATCH 17/50] fix laggy integ --- .../integration/tests/echo-data-server/api-key.client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/integration/tests/echo-data-server/api-key.client.test.ts b/packages/tests/integration/tests/echo-data-server/api-key.client.test.ts index 5b92f65e6..7ef68bf8f 100644 --- a/packages/tests/integration/tests/echo-data-server/api-key.client.test.ts +++ b/packages/tests/integration/tests/echo-data-server/api-key.client.test.ts @@ -45,7 +45,7 @@ describe('API Key Client', () => { console.log('🔄 Second balance check: ', secondBalanceCheck); expect(secondBalanceCheck.totalPaid).toBe(balanceCheck.totalPaid); - expect(secondBalanceCheck.totalSpent).toBeGreaterThan( + expect(secondBalanceCheck.totalSpent).toBeGreaterThanOrEqual( balanceCheck.totalSpent ); expect(secondBalanceCheck.balance).toBeLessThan(balanceCheck.balance); From 34d7c38a030458788cd69a96c44c81fec3392a1f Mon Sep 17 00:00:00 2001 From: Leo Lou Date: Wed, 15 Oct 2025 13:58:33 +0000 Subject: [PATCH 18/50] feat: add 'claude-sonnet-4-5' to Anthropic and OpenRouter provoder model lists --- packages/sdk/ts/src/supported-models/chat/anthropic.ts | 9 ++++++++- packages/sdk/ts/src/supported-models/chat/openrouter.ts | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/sdk/ts/src/supported-models/chat/anthropic.ts b/packages/sdk/ts/src/supported-models/chat/anthropic.ts index 6dfdb650e..b669618e1 100644 --- a/packages/sdk/ts/src/supported-models/chat/anthropic.ts +++ b/packages/sdk/ts/src/supported-models/chat/anthropic.ts @@ -10,7 +10,8 @@ export type AnthropicModel = | 'claude-3-opus-20240229' | 'claude-opus-4-1-20250805' | 'claude-opus-4-20250514' - | 'claude-sonnet-4-20250514'; + | 'claude-sonnet-4-20250514' + | 'claude-sonnet-4-5-20250929'; export const AnthropicModels: SupportedModel[] = [ { @@ -67,4 +68,10 @@ export const AnthropicModels: SupportedModel[] = [ output_cost_per_token: 0.000015, provider: 'Anthropic', }, + { + model_id: 'claude-sonnet-4-5-20250929', + input_cost_per_token: 0.000003, + output_cost_per_token: 0.000015, + provider: 'Anthropic', + }, ]; diff --git a/packages/sdk/ts/src/supported-models/chat/openrouter.ts b/packages/sdk/ts/src/supported-models/chat/openrouter.ts index 4874b0ffe..6da906b13 100644 --- a/packages/sdk/ts/src/supported-models/chat/openrouter.ts +++ b/packages/sdk/ts/src/supported-models/chat/openrouter.ts @@ -27,6 +27,7 @@ export type OpenRouterModel = | 'anthropic/claude-opus-4' | 'anthropic/claude-opus-4.1' | 'anthropic/claude-sonnet-4' + | 'anthropic/claude-sonnet-4.5' | 'arcee-ai/coder-large' | 'arcee-ai/maestro-reasoning' | 'arcee-ai/spotlight' @@ -473,6 +474,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000015, provider: 'OpenRouter', }, + { + model_id: 'anthropic/claude-sonnet-4.5', + input_cost_per_token: 0.000003, + output_cost_per_token: 0.000015, + provider: 'OpenRouter', + }, { model_id: 'arcee-ai/coder-large', input_cost_per_token: 5e-7, From 949ea98e0a5789a66218fe4d4e227bfe67ace185 Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Wed, 15 Oct 2025 15:52:23 -0400 Subject: [PATCH 19/50] rm migrations until finalized --- .../migration.sql | 11 ----------- .../migration.sql | 19 ------------------- packages/app/control/prisma/schema.prisma | 5 +++++ 3 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql delete mode 100644 packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql diff --git a/packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql b/packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql deleted file mode 100644 index 3f281e016..000000000 --- a/packages/app/control/prisma/migrations/20251010175808_add_x402_video_gen/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- CreateTable -CREATE TABLE "VideoGenerationX402" ( - "id" UUID NOT NULL, - "videoId" TEXT NOT NULL, - "wallet" TEXT, - "cost" DECIMAL(65,30) NOT NULL, - "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "isFinal" BOOLEAN NOT NULL DEFAULT false, - - CONSTRAINT "VideoGenerationX402_pkey" PRIMARY KEY ("id") -); diff --git a/packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql b/packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql deleted file mode 100644 index 85554210c..000000000 --- a/packages/app/control/prisma/migrations/20251013151321_update_to_video_gen/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ -/* - Warnings: - - - You are about to drop the `VideoGenerationX402` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropTable -DROP TABLE "public"."VideoGenerationX402"; - --- CreateTable -CREATE TABLE "video_generation_x402" ( - "videoId" TEXT NOT NULL, - "wallet" TEXT, - "cost" DECIMAL(65,30) NOT NULL, - "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "isFinal" BOOLEAN NOT NULL DEFAULT false, - - CONSTRAINT "video_generation_x402_pkey" PRIMARY KEY ("videoId") -); diff --git a/packages/app/control/prisma/schema.prisma b/packages/app/control/prisma/schema.prisma index 87c7118b6..037f9d0e7 100644 --- a/packages/app/control/prisma/schema.prisma +++ b/packages/app/control/prisma/schema.prisma @@ -493,9 +493,14 @@ model OutboundEmailSent { model VideoGenerationX402 { videoId String @id wallet String? + userId String? @db.Uuid + echoAppId String? @db.Uuid cost Decimal createdAt DateTime @default(now()) @db.Timestamptz(6) + expiresAt DateTime @db.Timestamptz(6) isFinal Boolean @default(false) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade) @@map("video_generation_x402") } From b0a9890f64c5ca2fa6c2c343c4d8e2cd09f707bf Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Wed, 15 Oct 2025 17:06:08 -0400 Subject: [PATCH 20/50] some additional work on the table. Refunds working with x402 --- .../migration.sql | 19 +++++++++++++++++++ packages/app/control/prisma/schema.prisma | 4 +++- packages/app/server/src/handlers.ts | 1 + .../server/src/services/EchoControlService.ts | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 packages/app/control/prisma/migrations/20251015203154_add_video_generation_x402_table/migration.sql diff --git a/packages/app/control/prisma/migrations/20251015203154_add_video_generation_x402_table/migration.sql b/packages/app/control/prisma/migrations/20251015203154_add_video_generation_x402_table/migration.sql new file mode 100644 index 000000000..81c638484 --- /dev/null +++ b/packages/app/control/prisma/migrations/20251015203154_add_video_generation_x402_table/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "video_generation_x402" ( + "videoId" TEXT NOT NULL, + "wallet" TEXT, + "userId" UUID, + "echoAppId" UUID, + "cost" DECIMAL(65,30) NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMPTZ(6) NOT NULL, + "isFinal" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "video_generation_x402_pkey" PRIMARY KEY ("videoId") +); + +-- AddForeignKey +ALTER TABLE "video_generation_x402" ADD CONSTRAINT "video_generation_x402_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "video_generation_x402" ADD CONSTRAINT "video_generation_x402_echoAppId_fkey" FOREIGN KEY ("echoAppId") REFERENCES "echo_apps"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/app/control/prisma/schema.prisma b/packages/app/control/prisma/schema.prisma index 037f9d0e7..3e7c925e5 100644 --- a/packages/app/control/prisma/schema.prisma +++ b/packages/app/control/prisma/schema.prisma @@ -40,6 +40,7 @@ model User { latestFreeCreditsVersion Decimal? OutboundEmailSent OutboundEmailSent[] creditGrantCodeUsages CreditGrantCodeUsage[] + VideoGenerationX402 VideoGenerationX402[] @@map("users") } @@ -107,6 +108,7 @@ model EchoApp { appSessions AppSession[] payouts Payout[] OutboundEmailSent OutboundEmailSent[] + VideoGenerationX402 VideoGenerationX402[] @@map("echo_apps") } @@ -500,7 +502,7 @@ model VideoGenerationX402 { expiresAt DateTime @db.Timestamptz(6) isFinal Boolean @default(false) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade) @@map("video_generation_x402") } diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 723b93daf..366d5953a 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -118,6 +118,7 @@ export async function handleX402Request({ videoId: transaction.metadata.providerId, wallet: payload.authorization.from, cost: transaction.rawTransactionCost, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), }, }); } diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index 034f04492..1bf3f5c91 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` + From a2aec60021502a7c1d28e9fc0b46ea19bc8eb8ca Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Wed, 15 Oct 2025 17:21:42 -0400 Subject: [PATCH 21/50] keep track of echo video gen that are non x402 --- packages/app/server/src/handlers.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 366d5953a..232684322 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -211,6 +211,18 @@ export async function handleApiKeyRequest({ isStream ); + if (provider.getType() === ProviderType.OPENAI_VIDEOS) { + await prisma.videoGenerationX402.create({ + data: { + videoId: transaction.metadata.providerId, + userId: echoControlService.getUserId()!, + echoAppId: echoControlService.getEchoAppId()!, + cost: transaction.rawTransactionCost, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), + }, + }); + } + // There is no actual refund, this logs if we underestimate the raw cost calculateRefundAmount(maxCost, transaction.rawTransactionCost); From 83923238a4bfcafc509b9cdfae3c409b12c5ab8b Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Wed, 15 Oct 2025 17:38:51 -0400 Subject: [PATCH 22/50] log errors on echo video gen, store total cost not rawtransaction cost --- packages/app/server/src/handlers.ts | 19 +++++++++++-------- .../src/providers/OpenAIVideoProvider.ts | 4 ++++ .../server/src/services/EchoControlService.ts | 6 +++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 232684322..d8138f3b2 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -211,22 +211,25 @@ export async function handleApiKeyRequest({ isStream ); + + + // There is no actual refund, this logs if we underestimate the raw cost + calculateRefundAmount(maxCost, transaction.rawTransactionCost); + + modelRequestService.handleResolveResponse(res, isStream, data); + + await echoControlService.createTransaction(transaction, maxCost); + if (provider.getType() === ProviderType.OPENAI_VIDEOS) { + const transactionCost = await echoControlService.computeTransactionCosts(transaction, null); await prisma.videoGenerationX402.create({ data: { videoId: transaction.metadata.providerId, userId: echoControlService.getUserId()!, echoAppId: echoControlService.getEchoAppId()!, - cost: transaction.rawTransactionCost, + cost: transactionCost.totalTransactionCost, expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), }, }); } - - // There is no actual refund, this logs if we underestimate the raw cost - calculateRefundAmount(maxCost, transaction.rawTransactionCost); - - modelRequestService.handleResolveResponse(res, isStream, data); - - await echoControlService.createTransaction(transaction, maxCost); } diff --git a/packages/app/server/src/providers/OpenAIVideoProvider.ts b/packages/app/server/src/providers/OpenAIVideoProvider.ts index aacc906d5..155dbf050 100644 --- a/packages/app/server/src/providers/OpenAIVideoProvider.ts +++ b/packages/app/server/src/providers/OpenAIVideoProvider.ts @@ -226,6 +226,10 @@ export class OpenAIVideoProvider extends BaseProvider { const refundAmount = decimalToUsdcBigInt(video.cost); await transfer(video.wallet as `0x${string}`, refundAmount); } + if (video.userId) { + // Proccess the refund to the user. There is some level of complexity here since there is a markup. Not as simple as just credit grant. + logger.info(`Refunding video generation ${video.videoId} to user ${video.userId} on app ${video.echoAppId}`); + } await tx.videoGenerationX402.update({ where: { videoId: video.videoId, diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index 1bf3f5c91..558c4fd76 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` + @@ -309,11 +309,11 @@ export class EchoControlService { markUpProfit, } = await this.computeTransactionCosts(transaction, this.referralCodeId); - logger.error( + logger.info( `Transaction cost: ${rawTransactionCost}, Max cost: ${maxCost}` ); if (rawTransactionCost.greaterThan(maxCost)) { - logger.error(` Difference: ${rawTransactionCost.minus(maxCost)}`); + logger.warn(` Difference: ${rawTransactionCost.minus(maxCost)}`); } const { userId, echoAppId, apiKeyId } = this.authResult; From 05e59cf68ed70b385af6641245e59dd7d4a0a40b Mon Sep 17 00:00:00 2001 From: Alvaro Echevarria Cuesta Date: Wed, 15 Oct 2025 18:15:37 -0400 Subject: [PATCH 23/50] download only available for 1 hour according to docs --- packages/app/server/src/handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 47b9b11b9..72f57c09c 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -151,7 +151,7 @@ export async function handleX402Request({ videoId: transaction.metadata.providerId, wallet: payload.authorization.from, cost: transaction.rawTransactionCost, - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 1), }, }); } @@ -232,7 +232,7 @@ export async function handleApiKeyRequest({ userId: echoControlService.getUserId()!, echoAppId: echoControlService.getEchoAppId()!, cost: transactionCost.totalTransactionCost, - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 1), }, }); } From 30cc7a9f49ebab94097c538d43a08a5709b28677 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 15 Oct 2025 18:48:28 -0400 Subject: [PATCH 24/50] use shared object --- .../middleware/transaction-escrow-middleware.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/app/server/src/middleware/transaction-escrow-middleware.ts b/packages/app/server/src/middleware/transaction-escrow-middleware.ts index ebcd5244d..92a21116b 100644 --- a/packages/app/server/src/middleware/transaction-escrow-middleware.ts +++ b/packages/app/server/src/middleware/transaction-escrow-middleware.ts @@ -197,10 +197,10 @@ export class TransactionEscrowMiddleware { userId: string, echoAppId: string, requestId: string, - cleanupExecuted: boolean + cleanupState: { executed: boolean } ) => { - if (cleanupExecuted) return; - cleanupExecuted = true; + if (cleanupState.executed) return; + cleanupState.executed = true; // decrementInFlightRequests now handles its own errors gracefully await this.decrementInFlightRequests(userId, echoAppId); @@ -215,21 +215,23 @@ export class TransactionEscrowMiddleware { echoAppId: string, requestId: string ) { - let cleanupExecuted = false; + // Use object to share state by reference across multiple event handlers + // This prevents duplicate cleanup execution when multiple events fire + const cleanupState = { executed: false }; // Cleanup on response finish (normal case) res.on('finish', () => - this.executeCleanup(userId, echoAppId, requestId, cleanupExecuted) + this.executeCleanup(userId, echoAppId, requestId, cleanupState) ); // Cleanup on response close (client disconnect) res.on('close', () => - this.executeCleanup(userId, echoAppId, requestId, cleanupExecuted) + this.executeCleanup(userId, echoAppId, requestId, cleanupState) ); // Cleanup on error (if response errors out) res.on('error', () => - this.executeCleanup(userId, echoAppId, requestId, cleanupExecuted) + this.executeCleanup(userId, echoAppId, requestId, cleanupState) ); } From 6d167521cbb03c6f584d16aeab6221e070c7943c Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Wed, 15 Oct 2025 19:03:30 -0400 Subject: [PATCH 25/50] clean abstraction --- packages/app/server/src/handlers.ts | 19 ++- .../app/server/src/resources/e2b/route.ts | 63 ++----- packages/app/server/src/resources/handler.ts | 155 ++++++++++++++++++ .../src/resources/tavily/crawl/route.ts | 66 ++------ .../src/resources/tavily/extract/route.ts | 67 ++------ .../src/resources/tavily/extract/tavily.ts | 6 +- .../src/resources/tavily/search/route.ts | 69 ++------ 7 files changed, 218 insertions(+), 227 deletions(-) create mode 100644 packages/app/server/src/resources/handler.ts diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index d45194496..66579ba6a 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -25,6 +25,19 @@ import { Decimal } from '@prisma/client/runtime/library'; import logger from 'logger'; import { Request, Response } from 'express'; +export async function refund( + paymentAmountDecimal: Decimal, + payload: ExactEvmPayload +) { + try { + const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); + const authPayload = payload.authorization; + await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); + } catch (error) { + logger.error('Failed to refund', error); + } +} + export async function settle( req: Request, res: Response, @@ -154,11 +167,7 @@ export async function handleX402Request({ payload ); } catch (error) { - const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); - const authPayload = payload.authorization; - await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt).catch((error) => { - console.error('Failed to transfer refund amount:', error); - }); + await refund(paymentAmountDecimal, payload); } } diff --git a/packages/app/server/src/resources/e2b/route.ts b/packages/app/server/src/resources/e2b/route.ts index eff51706c..9319e7f5a 100644 --- a/packages/app/server/src/resources/e2b/route.ts +++ b/packages/app/server/src/resources/e2b/route.ts @@ -1,58 +1,17 @@ -import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { Request, Response } from 'express'; +import { Decimal } from '@prisma/client/runtime/library'; import { E2BExecuteInputSchema } from './types'; import { calculateE2BExecuteCost, e2bExecutePythonSnippet, createE2BTransaction } from './e2b'; -import { authenticateRequest } from 'auth'; -import { prisma } from 'server'; -import { settle } from 'handlers'; -import { finalize } from 'handlers'; -import { Decimal } from '@prisma/client/runtime/library'; -import { Request, Response } from 'express'; +import { handleResourceRequestWithErrorHandling } from '../handler'; export async function e2bExecuteRoute(req: Request, res: Response) { - const headers = req.headers as Record; - - const maxCost = calculateE2BExecuteCost(); - - if (!isApiRequest(headers) && !isX402Request(headers)) { - return buildX402Response(req, res, maxCost); - } - - const inputBody = E2BExecuteInputSchema.safeParse(req.body); - if (!inputBody.success) { - return res.status(400).json({ error: 'Invalid body', issues: inputBody.error.issues }); - } - const parsedBody = inputBody.data; - - if (isApiRequest(headers)) { - const { echoControlService } = await authenticateRequest(headers, prisma); - - const output = await e2bExecutePythonSnippet(parsedBody.snippet); - - const actualCost = new Decimal(output.cost); - const transaction = createE2BTransaction(parsedBody, output, actualCost); - - await echoControlService.createTransaction(transaction, actualCost); - - 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 e2bExecutePythonSnippet(parsedBody.snippet); - - const actualCost = new Decimal(output.cost); - const transaction = createE2BTransaction(parsedBody, output, actualCost); - - finalize(paymentAmountDecimal, transaction, payload).catch((error) => { - console.error('Failed to finalize transaction:', error); - }); - - return res.status(200).json(output); - } else { - return buildX402Response(req, res, maxCost); - } + return handleResourceRequestWithErrorHandling(req, res, { + inputSchema: E2BExecuteInputSchema, + calculateMaxCost: () => calculateE2BExecuteCost(), + executeResource: (input) => e2bExecutePythonSnippet(input.snippet), + calculateActualCost: (_input, output) => new Decimal(output.cost), + createTransaction: createE2BTransaction, + errorMessage: 'Error executing e2b code', + }); } diff --git a/packages/app/server/src/resources/handler.ts b/packages/app/server/src/resources/handler.ts new file mode 100644 index 000000000..03629a403 --- /dev/null +++ b/packages/app/server/src/resources/handler.ts @@ -0,0 +1,155 @@ +import { Request, Response } from 'express'; +import { ZodSchema } from 'zod'; +import { Decimal } from '@prisma/client/runtime/library'; +import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { authenticateRequest } from 'auth'; +import { prisma } from 'server'; +import { settle, finalize, refund } from 'handlers'; +import logger from 'logger'; +import { ExactEvmPayload } from 'services/facilitator/x402-types'; + +type ResourceHandlerConfig = { + inputSchema: ZodSchema; + calculateMaxCost: (input?: TInput) => Decimal; + executeResource: (input: TInput) => Promise; + calculateActualCost: (input: TInput, output: TOutput) => Decimal; + createTransaction: (input: TInput, output: TOutput, cost: Decimal) => any; + errorMessage: string; +}; + +async function handleApiRequest( + parsedBody: TInput, + headers: Record, + config: ResourceHandlerConfig +) { + const { executeResource, calculateActualCost, createTransaction } = config; + + const { echoControlService } = await authenticateRequest(headers, prisma); + + const output = await executeResource(parsedBody); + + const actualCost = calculateActualCost(parsedBody, output); + const transaction = createTransaction(parsedBody, output, actualCost); + + await echoControlService.createTransaction(transaction, actualCost); + + return output; +} + +async function handle402Request( + req: Request, + res: Response, + parsedBody: TInput, + headers: Record, + safeMaxCost: Decimal, + maxCost: Decimal, + config: ResourceHandlerConfig +): Promise<{ output: TOutput } | { error: true }> { + const { executeResource, calculateActualCost, createTransaction } = config; + + const settleResult = await settle(req, res, headers, safeMaxCost); + if (!settleResult) { + return { error: true }; + } + + const { payload, paymentAmountDecimal } = settleResult; + + const output = await executeResourceWithRefund( + parsedBody, + executeResource, + paymentAmountDecimal, + payload + ); + + const actualCost = calculateActualCost(parsedBody, output); + const transaction = createTransaction(parsedBody, output, actualCost); + + finalize(paymentAmountDecimal, transaction, payload).catch((error) => { + logger.error('Failed to finalize transaction', error); + }); + + return { output }; +} + +async function executeResourceWithRefund( + parsedBody: TInput, + executeResource: (input: TInput) => Promise, + paymentAmountDecimal: Decimal, + payload: ExactEvmPayload +): Promise { + try { + const output = await executeResource(parsedBody); + return output; + } catch (error) { + await refund(paymentAmountDecimal, payload); + throw error; + } +} + +export async function handleResourceRequest( + req: Request, + res: Response, + config: ResourceHandlerConfig +) { + const { inputSchema, calculateMaxCost } = config; + + const headers = req.headers as Record; + + const inputBody = inputSchema.safeParse(req.body); + const maxCost = calculateMaxCost(inputBody.data); + + if (!isApiRequest(headers) && !isX402Request(headers)) { + return buildX402Response(req, res, maxCost); + } + + if (!inputBody.success) { + return res.status(400).json({ error: 'Invalid body', issues: inputBody.error.issues }); + } + + const parsedBody = inputBody.data; + const safeMaxCost = calculateMaxCost(parsedBody); + + if (isApiRequest(headers)) { + try { + const output = await handleApiRequest(parsedBody, headers, config); + return res.status(200).json(output); + } catch (error) { + logger.error('Failed to handle API request', error); + return res.status(500).json({ error: 'Internal server error' }); + } + } + + if (isX402Request(headers)) { + const result = await handle402Request( + req, + res, + parsedBody, + headers, + safeMaxCost, + maxCost, + config + ); + + if ('error' in result) { + return buildX402Response(req, res, maxCost); + } + + return res.status(200).json(result.output); + } + + return buildX402Response(req, res, safeMaxCost); +} + +export async function handleResourceRequestWithErrorHandling( + req: Request, + res: Response, + config: ResourceHandlerConfig +) { + const { errorMessage } = config; + + return handleResourceRequest(req, res, config).catch((error) => { + logger.error(errorMessage, error); + return res.status(500).json({ error: 'Internal server error' }); + }); +} + diff --git a/packages/app/server/src/resources/tavily/crawl/route.ts b/packages/app/server/src/resources/tavily/crawl/route.ts index a5574dc1b..84d646225 100644 --- a/packages/app/server/src/resources/tavily/crawl/route.ts +++ b/packages/app/server/src/resources/tavily/crawl/route.ts @@ -1,65 +1,21 @@ -import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { Request, Response } from 'express'; import { TavilyCrawlInputSchema } from './types'; import { calculateTavilyCrawlMaxCost, calculateTavilyCrawlActualCost, tavilyCrawl, + createTavilyTransaction, } from './tavily'; -import { authenticateRequest } from 'auth'; -import { prisma } from 'server'; -import { settle } from 'handlers'; -import { finalize } from 'handlers'; -import { createTavilyTransaction } from './tavily'; -import { Request, Response } from 'express'; +import { handleResourceRequestWithErrorHandling } from '../../handler'; export async function tavilyCrawlRoute(req: Request, res: Response) { - const headers = req.headers as Record; - - const inputBody = TavilyCrawlInputSchema.safeParse(req.body); - - const maxCost = calculateTavilyCrawlMaxCost(inputBody.data); - - if (!isApiRequest(headers) && !isX402Request(headers)) { - return buildX402Response(req, res, maxCost); - } - - if (!inputBody.success) { - return res - .status(400) - .json({ error: 'Invalid body', issues: inputBody.error.issues }); - } - - const parsedBody = inputBody.data; - const safeMaxCost = calculateTavilyCrawlMaxCost(parsedBody); - - if (isApiRequest(headers)) { - const { echoControlService } = await authenticateRequest(headers, prisma); - - const output = await tavilyCrawl(parsedBody); - - const actualCost = calculateTavilyCrawlActualCost(parsedBody, output); - const transaction = createTavilyTransaction(parsedBody, output, actualCost); - - await echoControlService.createTransaction(transaction, actualCost); - - return res.status(200).json(output); - } else if (isX402Request(headers)) { - const settleResult = await settle(req, res, headers, safeMaxCost); - if (!settleResult) { - return buildX402Response(req, res, maxCost); - } - const { payload, paymentAmountDecimal } = settleResult; - - const output = await tavilyCrawl(parsedBody); - - const actualCost = calculateTavilyCrawlActualCost(parsedBody, output); - const transaction = createTavilyTransaction(parsedBody, output, actualCost); - - await finalize(paymentAmountDecimal, transaction, payload); - - return res.status(200).json(output); - } else { - return buildX402Response(req, res, safeMaxCost); - } + return handleResourceRequestWithErrorHandling(req, res, { + inputSchema: TavilyCrawlInputSchema, + calculateMaxCost: (input) => calculateTavilyCrawlMaxCost(input), + executeResource: tavilyCrawl, + calculateActualCost: calculateTavilyCrawlActualCost, + createTransaction: createTavilyTransaction, + errorMessage: 'Error crawling tavily', + }); } diff --git a/packages/app/server/src/resources/tavily/extract/route.ts b/packages/app/server/src/resources/tavily/extract/route.ts index 57d496a0f..25f0b5545 100644 --- a/packages/app/server/src/resources/tavily/extract/route.ts +++ b/packages/app/server/src/resources/tavily/extract/route.ts @@ -1,66 +1,21 @@ -import { buildX402Response, isApiRequest, isX402Request } from 'utils'; +import { Request, Response } from 'express'; import { TavilyExtractInputSchema } from './types'; import { calculateTavilyExtractMaxCost, calculateTavilyExtractActualCost, tavilyExtract, + createTavilyTransaction, } 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'; +import { handleResourceRequestWithErrorHandling } from '../../handler'; export async function tavilyExtractRoute(req: Request, res: Response) { - const headers = req.headers as Record; - - const inputBody = TavilyExtractInputSchema.safeParse(req.body); - - const maxCost = calculateTavilyExtractMaxCost(inputBody.data); - - if (!isApiRequest(headers) && !isX402Request(headers)) { - return buildX402Response(req, res, maxCost); - } - - if (!inputBody.success) { - return res - .status(400) - .json({ error: 'Invalid body', issues: inputBody.error.issues }); - } - - const parsedBody = inputBody.data; - const safeMaxCost = calculateTavilyExtractMaxCost(parsedBody); - - if (isApiRequest(headers)) { - const { echoControlService } = await authenticateRequest(headers, prisma); - - const output = await tavilyExtract(parsedBody); - - const actualCost = calculateTavilyExtractActualCost(parsedBody, output); - const transaction = createTavilyTransaction(parsedBody, output, actualCost); - - await echoControlService.createTransaction(transaction, actualCost); - - return res.status(200).json(output); - } else if (isX402Request(headers)) { - const settleResult = await settle(req, res, headers, safeMaxCost); - if (!settleResult) { - return buildX402Response(req, res, maxCost); - } - const { payload, paymentAmountDecimal } = settleResult; - - const output = await tavilyExtract(parsedBody); - - const actualCost = calculateTavilyExtractActualCost(parsedBody, output); - const transaction = createTavilyTransaction(parsedBody, output, actualCost); - - await finalize(paymentAmountDecimal, transaction, payload); - - return res.status(200).json(output); - } else { - return buildX402Response(req, res, safeMaxCost); - } + return handleResourceRequestWithErrorHandling(req, res, { + inputSchema: TavilyExtractInputSchema, + calculateMaxCost: (input) => calculateTavilyExtractMaxCost(input), + executeResource: tavilyExtract, + calculateActualCost: calculateTavilyExtractActualCost, + createTransaction: createTavilyTransaction, + errorMessage: 'Error extracting tavily', + }); } diff --git a/packages/app/server/src/resources/tavily/extract/tavily.ts b/packages/app/server/src/resources/tavily/extract/tavily.ts index 8b8ef1822..971c207c2 100644 --- a/packages/app/server/src/resources/tavily/extract/tavily.ts +++ b/packages/app/server/src/resources/tavily/extract/tavily.ts @@ -6,6 +6,7 @@ import { TavilyExtractOutputSchema, } from './types'; import { Transaction } from '../../../types'; +import { HttpError } from 'errors/http'; export const calculateTavilyExtractMaxCost = ( input: TavilyExtractInput | undefined @@ -58,6 +59,8 @@ export const createTavilyTransaction = ( const TAVILY_API_KEY = process.env.TAVILY_API_KEY; + + export async function tavilyExtract( input: TavilyExtractInput ): Promise { @@ -72,7 +75,8 @@ export async function tavilyExtract( if (!response.ok) { const errorText = await response.text(); - throw new Error( + throw new HttpError( + response.status, `Tavily API request failed: ${response.status} ${response.statusText} - ${errorText}` ); } diff --git a/packages/app/server/src/resources/tavily/search/route.ts b/packages/app/server/src/resources/tavily/search/route.ts index 1db9e2fe5..ea33c96e8 100644 --- a/packages/app/server/src/resources/tavily/search/route.ts +++ b/packages/app/server/src/resources/tavily/search/route.ts @@ -1,62 +1,15 @@ -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'; +import { TavilySearchInputSchema } from './types'; +import { calculateTavilySearchCost, tavilySearch, createTavilyTransaction } from './tavily'; +import { handleResourceRequestWithErrorHandling } from '../../handler'; export async function tavilySearchRoute(req: Request, res: Response) { - try { - const headers = req.headers as Record; - - const inputBody = TavilySearchInputSchema.safeParse(req.body); - - const maxCost = calculateTavilySearchCost(inputBody.data); - - if (!isApiRequest(headers) && !isX402Request(headers)) { - return buildX402Response(req, res, maxCost); - } - - if (!inputBody.success) { - return res.status(400).json({ error: 'Invalid body', issues: inputBody.error.issues }); - } - - const parsedBody = inputBody.data; - const safeMaxCost = calculateTavilySearchCost(parsedBody); - - if (isApiRequest(headers)) { - const { echoControlService } = await authenticateRequest(headers, prisma); - - const output = await tavilySearch(parsedBody); - - const transaction = createTavilyTransaction(parsedBody, output, safeMaxCost); - - await echoControlService.createTransaction(transaction, safeMaxCost); - - return res.status(200).json(output); - } else if (isX402Request(headers)) { - const settleResult = await settle(req, res, headers, safeMaxCost); - if (!settleResult) { - return buildX402Response(req, res, maxCost); - } - const { payload, paymentAmountDecimal } = settleResult; - - const output = await tavilySearch(parsedBody); - - const transaction = createTavilyTransaction(parsedBody, output, safeMaxCost); - - await finalize(paymentAmountDecimal, transaction, payload); - - return res.status(200).json(output); - } else { - return buildX402Response(req, res, safeMaxCost); - } - } catch (error) { - logger.error('Error searching tavily', error); - return res.status(500).json({ error: 'Internal server error' }); - } + return handleResourceRequestWithErrorHandling(req, res, { + inputSchema: TavilySearchInputSchema, + calculateMaxCost: (input) => calculateTavilySearchCost(input), + executeResource: tavilySearch, + calculateActualCost: (input) => calculateTavilySearchCost(input), + createTransaction: createTavilyTransaction, + errorMessage: 'Error searching tavily', + }); } From c610ce6069b3ecfcf1cef29bc5a2602220946644 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Wed, 15 Oct 2025 19:10:50 -0400 Subject: [PATCH 26/50] cleanup error handling --- packages/app/server/src/resources/e2b/e2b.ts | 53 +++++++++------ packages/app/server/src/resources/handler.ts | 67 +++++++++++-------- .../src/resources/tavily/crawl/tavily.ts | 4 +- .../src/resources/tavily/search/tavily.ts | 4 +- 4 files changed, 76 insertions(+), 52 deletions(-) diff --git a/packages/app/server/src/resources/e2b/e2b.ts b/packages/app/server/src/resources/e2b/e2b.ts index 0467b0afc..218c1d46b 100644 --- a/packages/app/server/src/resources/e2b/e2b.ts +++ b/packages/app/server/src/resources/e2b/e2b.ts @@ -4,6 +4,7 @@ import { E2BExecuteOutput, E2BExecuteInput } from "./types"; import { DEFAULT_VCPU_COUNT, PRICE_PER_VCPU_PER_SECOND } from "./prices"; import { Decimal } from "@prisma/client/runtime/library"; import { Transaction } from "../../types"; +import { HttpError } from 'errors/http'; dotenv.config(); export const calculateE2BExecuteCost = (): Decimal => { @@ -37,27 +38,35 @@ export const e2bExecutePythonSnippet = async (snippet: string): Promise = { inputSchema: ZodSchema; @@ -42,14 +43,13 @@ async function handle402Request( parsedBody: TInput, headers: Record, safeMaxCost: Decimal, - maxCost: Decimal, config: ResourceHandlerConfig -): Promise<{ output: TOutput } | { error: true }> { +): Promise { const { executeResource, calculateActualCost, createTransaction } = config; const settleResult = await settle(req, res, headers, safeMaxCost); if (!settleResult) { - return { error: true }; + throw new PaymentRequiredError('Payment required, settle failed'); } const { payload, paymentAmountDecimal } = settleResult; @@ -64,11 +64,11 @@ async function handle402Request( const actualCost = calculateActualCost(parsedBody, output); const transaction = createTransaction(parsedBody, output, actualCost); - finalize(paymentAmountDecimal, transaction, payload).catch((error) => { + finalize(paymentAmountDecimal, transaction, payload).catch(error => { logger.error('Failed to finalize transaction', error); }); - return { output }; + return output; } async function executeResourceWithRefund( @@ -77,8 +77,8 @@ async function executeResourceWithRefund( paymentAmountDecimal: Decimal, payload: ExactEvmPayload ): Promise { - try { - const output = await executeResource(parsedBody); + try { + const output = await executeResource(parsedBody); return output; } catch (error) { await refund(paymentAmountDecimal, payload); @@ -103,7 +103,9 @@ export async function handleResourceRequest( } if (!inputBody.success) { - return res.status(400).json({ error: 'Invalid body', issues: inputBody.error.issues }); + return res + .status(400) + .json({ error: 'Invalid body', issues: inputBody.error.issues }); } const parsedBody = inputBody.data; @@ -120,21 +122,24 @@ export async function handleResourceRequest( } if (isX402Request(headers)) { - const result = await handle402Request( - req, - res, - parsedBody, - headers, - safeMaxCost, - maxCost, - config - ); - - if ('error' in result) { - return buildX402Response(req, res, maxCost); + try { + const result = await handle402Request( + req, + res, + parsedBody, + headers, + safeMaxCost, + config + ); + return res.status(200).json(result); + } catch (error) { + if (error instanceof PaymentRequiredError) { + logger.error('Failed to handle 402 request', error); + return buildX402Response(req, res, safeMaxCost); + } + logger.error('Failed to handle 402 request', error); + return res.status(500).json({ error: 'Internal server error' }); } - - return res.status(200).json(result.output); } return buildX402Response(req, res, safeMaxCost); @@ -145,11 +150,17 @@ export async function handleResourceRequestWithErrorHandling( res: Response, config: ResourceHandlerConfig ) { - const { errorMessage } = config; - - return handleResourceRequest(req, res, config).catch((error) => { + try { + return await handleResourceRequest(req, res, config); + } catch (error) { + const { errorMessage } = config; + if (error instanceof HttpError) { + logger.error(errorMessage, error); + return res.status(error.statusCode).json({ error: errorMessage }); + } logger.error(errorMessage, error); - return res.status(500).json({ error: 'Internal server error' }); - }); + return res + .status(500) + .json({ error: errorMessage || 'Internal server error' }); + } } - diff --git a/packages/app/server/src/resources/tavily/crawl/tavily.ts b/packages/app/server/src/resources/tavily/crawl/tavily.ts index 0cfedefd3..95ad0a1ab 100644 --- a/packages/app/server/src/resources/tavily/crawl/tavily.ts +++ b/packages/app/server/src/resources/tavily/crawl/tavily.ts @@ -6,6 +6,7 @@ import { TavilyCrawlOutputSchema, } from './types'; import { Transaction } from '../../../types'; +import { HttpError } from 'errors/http'; export const calculateTavilyCrawlMaxCost = ( input: TavilyCrawlInput | undefined @@ -91,7 +92,8 @@ export async function tavilyCrawl( if (!response.ok) { const errorText = await response.text(); - throw new Error( + throw new HttpError( + response.status, `Tavily API request failed: ${response.status} ${response.statusText} - ${errorText}` ); } diff --git a/packages/app/server/src/resources/tavily/search/tavily.ts b/packages/app/server/src/resources/tavily/search/tavily.ts index cc56b9da9..8ff82f6e2 100644 --- a/packages/app/server/src/resources/tavily/search/tavily.ts +++ b/packages/app/server/src/resources/tavily/search/tavily.ts @@ -6,6 +6,7 @@ import { TavilySearchOutputSchema, } from './types'; import { Transaction } from '../../../types'; +import { HttpError } from 'errors/http'; export const calculateTavilySearchCost = ( input: TavilySearchInput | undefined @@ -48,7 +49,8 @@ export async function tavilySearch( if (!response.ok) { const errorText = await response.text(); - throw new Error( + throw new HttpError( + response.status, `Tavily API request failed: ${response.status} ${response.statusText} - ${errorText}` ); } From 447a30a05062e88ca4b9d1d28f740af64aaecdd5 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Wed, 15 Oct 2025 21:16:16 -0400 Subject: [PATCH 27/50] bump sdk versions for sonnet 4.5 --- packages/sdk/next/package.json | 2 +- packages/sdk/react/package.json | 2 +- packages/sdk/ts/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/next/package.json b/packages/sdk/next/package.json index a247ce341..ce0b34298 100644 --- a/packages/sdk/next/package.json +++ b/packages/sdk/next/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-next-sdk", - "version": "0.0.26", + "version": "0.0.27", "description": "Next.js SDK for Echo", "type": "module", "main": "./dist/index.js", diff --git a/packages/sdk/react/package.json b/packages/sdk/react/package.json index de0618dfb..6422b45e1 100644 --- a/packages/sdk/react/package.json +++ b/packages/sdk/react/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-react-sdk", - "version": "1.0.35", + "version": "1.0.36", "description": "React SDK for Echo OAuth2 + PKCE authentication and token management", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index ed8f5d1cf..54143b854 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-typescript-sdk", - "version": "1.0.19", + "version": "1.0.20", "description": "TypeScript SDK for Echo platform", "type": "module", "main": "./dist/index.cjs", From 22150b4cdb48733a9d09179b208ebae152c4b56f Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Wed, 15 Oct 2025 21:21:42 -0400 Subject: [PATCH 28/50] fix filename --- ...generate-text.test copy.ts => anthropic-generate-text.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/tests/provider-smoke/{anthropic-generate-text.test copy.ts => anthropic-generate-text.test.ts} (100%) diff --git a/packages/tests/provider-smoke/anthropic-generate-text.test copy.ts b/packages/tests/provider-smoke/anthropic-generate-text.test.ts similarity index 100% rename from packages/tests/provider-smoke/anthropic-generate-text.test copy.ts rename to packages/tests/provider-smoke/anthropic-generate-text.test.ts From 5d63e2fe489c38ef4706fb5073a71e7678d619fa Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Wed, 15 Oct 2025 21:29:12 -0400 Subject: [PATCH 29/50] provider smoke test --- .github/workflows/provider-smoke-tests.yml | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/provider-smoke-tests.yml diff --git a/.github/workflows/provider-smoke-tests.yml b/.github/workflows/provider-smoke-tests.yml new file mode 100644 index 000000000..ebb1233e8 --- /dev/null +++ b/.github/workflows/provider-smoke-tests.yml @@ -0,0 +1,69 @@ +name: Provider Smoke Tests + +on: + push: + branches: [master] + paths: + - 'packages/tests/provider-smoke/**' + - 'packages/sdk/ts/**' + - 'packages/app/server/**' + - 'packages/app/control/**' + - '.github/workflows/provider-smoke-tests.yml' + +jobs: + provider-smoke-tests: + name: Run Provider Smoke Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install pnpm + run: npm install -g pnpm + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + + - name: Wait for Railway Deployment + env: + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + run: | + echo "Installing Railway CLI..." + npm install -g @railway/cli + + echo "Waiting for Railway deployment to complete..." + railway status --service echo --environment staging || echo "Service status check failed, continuing..." + + echo "Waiting for deployment to be ready..." + max_attempts=30 + attempt=0 + while [ $attempt -lt $max_attempts ]; do + if curl -f -s -o /dev/null https://echo-staging.up.railway.app/health 2>/dev/null; then + echo "Deployment is ready!" + break + fi + attempt=$((attempt + 1)) + echo "Attempt $attempt/$max_attempts: Deployment not ready yet, waiting 10 seconds..." + sleep 10 + done + + if [ $attempt -eq $max_attempts ]; then + echo "Deployment did not become ready in time" + exit 1 + fi + + - name: Run Provider Smoke Tests + working-directory: ./packages/tests/provider-smoke + env: + ECHO_DATA_SERVER_URL: https://echo-staging.up.railway.app/ + ECHO_API_KEY: ${{ secrets.ECHO_API_KEY }} + ECHO_APP_ID: a4e9b928-cac0-4952-9b4e-3be01aaff45b + run: pnpm run test From 877125d8d384fcc0d92a3c50d619ce5f0132a6db Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Wed, 15 Oct 2025 21:32:29 -0400 Subject: [PATCH 30/50] increase timeout to 15m --- .github/workflows/provider-smoke-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/provider-smoke-tests.yml b/.github/workflows/provider-smoke-tests.yml index ebb1233e8..2869ff26c 100644 --- a/.github/workflows/provider-smoke-tests.yml +++ b/.github/workflows/provider-smoke-tests.yml @@ -14,7 +14,7 @@ jobs: provider-smoke-tests: name: Run Provider Smoke Tests runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 40 steps: - name: Checkout code @@ -43,7 +43,7 @@ jobs: railway status --service echo --environment staging || echo "Service status check failed, continuing..." echo "Waiting for deployment to be ready..." - max_attempts=30 + max_attempts=90 attempt=0 while [ $attempt -lt $max_attempts ]; do if curl -f -s -o /dev/null https://echo-staging.up.railway.app/health 2>/dev/null; then @@ -56,7 +56,7 @@ jobs: done if [ $attempt -eq $max_attempts ]; then - echo "Deployment did not become ready in time" + echo "Deployment did not become ready in time (waited 15 minutes)" exit 1 fi From d5b2b051434ffdc12deae9f9d21c47a9a4022bd9 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Thu, 16 Oct 2025 21:23:28 +0530 Subject: [PATCH 31/50] Add Groq provider support --- packages/app/control/package.json | 2 +- .../app/server/src/providers/GroqProvider.ts | 125 ++++++++++++++++++ .../server/src/providers/ProviderFactory.ts | 6 + .../app/server/src/providers/ProviderType.ts | 1 + .../server/src/services/AccountingService.ts | 2 + packages/sdk/ts/src/index.ts | 2 + .../sdk/ts/src/supported-models/chat/groq.ts | 48 +++++++ 7 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 packages/app/server/src/providers/GroqProvider.ts create mode 100644 packages/sdk/ts/src/supported-models/chat/groq.ts diff --git a/packages/app/control/package.json b/packages/app/control/package.json index cc8a10096..047feaf6a 100644 --- a/packages/app/control/package.json +++ b/packages/app/control/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "predev": "docker-compose -f docker-local-db.yml up -d && sleep 3 && prisma generate && prisma migrate deploy", + "predev": "docker compose -f docker-local-db.yml up -d && sleep 3 && prisma generate && prisma migrate deploy", "dev": "next dev --turbopack", "prebuild": "prisma generate", "build": "next build", diff --git a/packages/app/server/src/providers/GroqProvider.ts b/packages/app/server/src/providers/GroqProvider.ts new file mode 100644 index 000000000..31c75371c --- /dev/null +++ b/packages/app/server/src/providers/GroqProvider.ts @@ -0,0 +1,125 @@ +import { LlmTransactionMetadata, Transaction } from '../types'; +import { getCostPerToken } from '../services/AccountingService'; +import { BaseProvider } from './BaseProvider'; +import { ProviderType } from './ProviderType'; +import logger from '../logger'; + +export interface CompletionStateBody { + id: string; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +export interface StreamingChunkBody { + id: string; + choices: { + index: number; + delta: { + content?: string; + }; + finish_reason: string | null; + }[]; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + } | null; +} + +export const parseSSEGPTFormat = (data: string): StreamingChunkBody[] => { + const events = data.split('\n\n'); + const chunks: StreamingChunkBody[] = []; + + for (const event of events) { + if (!event.trim()) continue; + if (event.startsWith('data: ')) { + const jsonStr = event.slice(6); + if (jsonStr.trim() === '[DONE]') continue; + try { + const parsed = JSON.parse(jsonStr); + chunks.push(parsed); + } catch (error) { + logger.error(`Error parsing SSE chunk: ${error}`); + } + } + } + + return chunks; +}; + +export class GroqProvider extends BaseProvider { + private readonly GROQ_BASE_URL = 'https://api.groq.com/openai/v1'; + + getType(): ProviderType { + return ProviderType.GROQ; + } + + getBaseUrl(): string { + return this.GROQ_BASE_URL; + } + + getApiKey(): string | undefined { + return process.env.GROQ_API_KEY; + } + + override supportsStream(): boolean { + return true; + } + + async handleBody(data: string): Promise { + try { + let prompt_tokens = 0; + let completion_tokens = 0; + let total_tokens = 0; + let providerId = 'null'; + + if (this.getIsStream()) { + const chunks = parseSSEGPTFormat(data); + + for (const chunk of chunks) { + if (chunk.usage !== null) { + prompt_tokens += chunk.usage.prompt_tokens; + completion_tokens += chunk.usage.completion_tokens; + total_tokens += chunk.usage.total_tokens; + } + providerId = chunk.id || 'null'; + } + } else { + const parsed = JSON.parse(data) as CompletionStateBody; + prompt_tokens += parsed.usage.prompt_tokens; + completion_tokens += parsed.usage.completion_tokens; + total_tokens += parsed.usage.total_tokens; + providerId = parsed.id || 'null'; + } + + const cost = getCostPerToken( + this.getModel(), + prompt_tokens, + completion_tokens + ); + + const metadata: LlmTransactionMetadata = { + providerId: providerId, + provider: this.getType(), + model: this.getModel(), + inputTokens: prompt_tokens, + outputTokens: completion_tokens, + totalTokens: total_tokens, + }; + + const transaction: Transaction = { + rawTransactionCost: cost, + metadata: metadata, + status: 'success', + }; + + return transaction; + } catch (error) { + logger.error(`Error processing data: ${error}`); + throw error; + } + } +} diff --git a/packages/app/server/src/providers/ProviderFactory.ts b/packages/app/server/src/providers/ProviderFactory.ts index 7b6938dd9..7efcf3e6c 100644 --- a/packages/app/server/src/providers/ProviderFactory.ts +++ b/packages/app/server/src/providers/ProviderFactory.ts @@ -11,6 +11,7 @@ import type { BaseProvider } from './BaseProvider'; import { GeminiGPTProvider } from './GeminiGPTProvider'; import { GeminiProvider } from './GeminiProvider'; import { OpenAIVideoProvider } from './OpenAIVideoProvider'; +import { GroqProvider } from './GroqProvider'; import { GeminiVeoProvider, PROXY_PASSTHROUGH_ONLY_MODEL as GeminiVeoProxyPassthroughOnlyModel, @@ -48,6 +49,9 @@ const createChatModelToProviderMapping = (): Record => { case 'OpenRouter': mapping[modelConfig.model_id] = ProviderType.OPENROUTER; break; + case 'Groq': + mapping[modelConfig.model_id] = ProviderType.GROQ; + break; // Add other providers as needed default: // Skip models with unsupported providers @@ -178,6 +182,8 @@ export const getProvider = ( return new VertexAIProvider(stream, model); case ProviderType.OPENAI_VIDEOS: return new OpenAIVideoProvider(stream, model); + case ProviderType.GROQ: + return new GroqProvider(stream, model); default: throw new Error(`Unknown provider type: ${type}`); } diff --git a/packages/app/server/src/providers/ProviderType.ts b/packages/app/server/src/providers/ProviderType.ts index 0bad6e832..e8b006ab4 100644 --- a/packages/app/server/src/providers/ProviderType.ts +++ b/packages/app/server/src/providers/ProviderType.ts @@ -10,4 +10,5 @@ export enum ProviderType { OPENROUTER = 'OPENROUTER', OPENAI_IMAGES = 'OPENAI_IMAGES', OPENAI_VIDEOS = 'OPENAI_VIDEOS', + GROQ = 'GROQ', } diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index 117bc4906..8a3006475 100644 --- a/packages/app/server/src/services/AccountingService.ts +++ b/packages/app/server/src/services/AccountingService.ts @@ -3,6 +3,7 @@ import { AnthropicModels, GeminiModels, OpenRouterModels, + GroqModels, OpenAIImageModels, SupportedOpenAIResponseToolPricing, SupportedModel, @@ -26,6 +27,7 @@ export const ALL_SUPPORTED_MODELS: SupportedModel[] = [ ...AnthropicModels, ...GeminiModels, ...OpenRouterModels, + ...GroqModels, ]; // Handle image models separately since they have different pricing structure diff --git a/packages/sdk/ts/src/index.ts b/packages/sdk/ts/src/index.ts index 34323a6ee..9dd11ce4f 100644 --- a/packages/sdk/ts/src/index.ts +++ b/packages/sdk/ts/src/index.ts @@ -44,6 +44,8 @@ export { GeminiModels } from './supported-models/chat/gemini'; export type { GeminiModel } from './supported-models/chat/gemini'; export { OpenRouterModels } from './supported-models/chat/openrouter'; export type { OpenRouterModel } from './supported-models/chat/openrouter'; +export { GroqModels } from './supported-models/chat/groq'; +export type { GroqModel } from './supported-models/chat/groq'; export { OpenAIImageModels } from './supported-models/image/openai'; export type { OpenAIImageModel } from './supported-models/image/openai'; export { GeminiVideoModels } from './supported-models/video/gemini'; diff --git a/packages/sdk/ts/src/supported-models/chat/groq.ts b/packages/sdk/ts/src/supported-models/chat/groq.ts new file mode 100644 index 000000000..c94697a2d --- /dev/null +++ b/packages/sdk/ts/src/supported-models/chat/groq.ts @@ -0,0 +1,48 @@ +import { SupportedModel } from '../types'; + +export type GroqModel = + | 'llama3-8b-8192' + | 'llama3-70b-8192' + | 'mixtral-8x7b-32768' + | 'gemma2-9b-it' + | 'llama-3.1-8b-instant' + | 'llama-3.1-70b-versatile'; + +export const GroqModels: SupportedModel[] = [ + { + model_id: 'llama3-8b-8192', + input_cost_per_token: 0, + output_cost_per_token: 0, + provider: 'Groq', + }, + { + model_id: 'llama3-70b-8192', + input_cost_per_token: 0, + output_cost_per_token: 0, + provider: 'Groq', + }, + { + model_id: 'mixtral-8x7b-32768', + input_cost_per_token: 0, + output_cost_per_token: 0, + provider: 'Groq', + }, + { + model_id: 'gemma2-9b-it', + input_cost_per_token: 0, + output_cost_per_token: 0, + provider: 'Groq', + }, + { + model_id: 'llama-3.1-8b-instant', + input_cost_per_token: 0, + output_cost_per_token: 0, + provider: 'Groq', + }, + { + model_id: 'llama-3.1-70b-versatile', + input_cost_per_token: 0, + output_cost_per_token: 0, + provider: 'Groq', + }, +]; From c0056c0867a5a51cbac66b4d95c9d28946739085 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 12:01:20 -0400 Subject: [PATCH 32/50] update the tests --- .../server/src/services/EchoControlService.ts | 2 +- packages/sdk/next/package.json | 2 +- packages/sdk/react/package.json | 2 +- packages/sdk/ts/package.json | 2 +- .../ts/scripts/update-openrouter-models.ts | 77 +- .../ts/src/supported-models/chat/anthropic.ts | 7 + .../ts/src/supported-models/chat/gemini.ts | 66 +- .../ts/src/supported-models/chat/openai.ts | 98 ++ .../src/supported-models/chat/openrouter.ts | 1095 ++++++----------- .../gemini-generate-text.test.ts | 13 + .../provider-smoke/gemini-stream-text.test.ts | 7 +- .../openai-generate-text.test.ts | 29 +- .../provider-smoke/openai-stream-text.test.ts | 6 + .../openrouter-generate-text.test.ts | 7 +- .../openrouter-stream-text.test.ts | 17 +- packages/tests/provider-smoke/test-helpers.ts | 63 +- 16 files changed, 713 insertions(+), 780 deletions(-) diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index dca4e6558..8cbe67626 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` + diff --git a/packages/sdk/next/package.json b/packages/sdk/next/package.json index ce0b34298..0630525f1 100644 --- a/packages/sdk/next/package.json +++ b/packages/sdk/next/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-next-sdk", - "version": "0.0.27", + "version": "0.0.28", "description": "Next.js SDK for Echo", "type": "module", "main": "./dist/index.js", diff --git a/packages/sdk/react/package.json b/packages/sdk/react/package.json index 6422b45e1..fadb5d46c 100644 --- a/packages/sdk/react/package.json +++ b/packages/sdk/react/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-react-sdk", - "version": "1.0.36", + "version": "1.0.37", "description": "React SDK for Echo OAuth2 + PKCE authentication and token management", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index 54143b854..e2f4b42df 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-typescript-sdk", - "version": "1.0.20", + "version": "1.0.21", "description": "TypeScript SDK for Echo platform", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sdk/ts/scripts/update-openrouter-models.ts b/packages/sdk/ts/scripts/update-openrouter-models.ts index 2ea5ea9db..1bfbb88a9 100644 --- a/packages/sdk/ts/scripts/update-openrouter-models.ts +++ b/packages/sdk/ts/scripts/update-openrouter-models.ts @@ -5,42 +5,59 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; - -interface SupportedModel { - model_id: string; - input_cost_per_token: number; - output_cost_per_token: number; - provider: string; -} - +import { SupportedModel } from './update-models'; interface OpenRouterModel { id: string; + canonical_slug?: string; + hugging_face_id?: string; name: string; + created?: number; description?: string; - pricing: { - prompt: string; - completion: string; - }; context_length: number; architecture: { modality: string; + input_modalities?: string[]; + output_modalities?: string[]; tokenizer: string; - instruct_type?: string; + instruct_type?: string | null; }; - top_provider: { - max_completion_tokens?: number; - is_moderated: boolean; + pricing: { + prompt: string; + completion: string; + request?: string; + image?: string; + web_search?: string; + internal_reasoning?: string; }; - per_request_limits?: { - prompt_tokens: string; - completion_tokens: string; + top_provider: { + context_length?: number; + max_completion_tokens?: number | null; + is_moderated?: boolean; }; + per_request_limits?: unknown; + supported_parameters?: string[]; + default_parameters?: Record; } interface OpenRouterResponse { data: OpenRouterModel[]; } +const BLACKLISTED_FAST_FAIL_MODELS = new Set([ + 'anthracite-org/magnum-v2-72b', + 'arcee-ai/spotlight', + 'agentica-org/deepcoder-14b-preview', + 'relace/relace-apply-3', + 'nousresearch/hermes-3-llama-3.1-70b', + 'openai/gpt-4o-audio-preview', + 'qwen/qwen-2.5-coder-32b-instruct', + 'arliai/qwq-32b-arliai-rpr-v1', + 'qwen/qwen3-next-80b-a3b-thinking', + 'anthropic/claude-3.5-haiku-20241022', + 'minimax/minimax-01', + 'openrouter/auto', +]); + async function fetchOpenRouterModels(): Promise { try { console.log('📡 Fetching models from OpenRouter API...'); @@ -55,11 +72,24 @@ async function fetchOpenRouterModels(): Promise { const data: OpenRouterResponse = await response.json(); console.log(`🔍 Found ${data.data.length} models from OpenRouter API`); + console.log(`🔍 Models:`, data.data); // Filter for text models only and convert to our format const supportedModels: SupportedModel[] = []; for (const model of data.data) { + // Fast fail if the model is in the blacklist + if (BLACKLISTED_FAST_FAIL_MODELS.has(model.id)) { + console.log(`⏭️ Skipping ${model.id} - blacklisted fast fail`); + continue; + } + + // Skip free tier models + if (model.id.endsWith(':free')) { + console.log(` ⏭️ Skipping ${model.id} - free tier model`); + continue; + } + // Only include text-based models if ( model.architecture.modality === 'text->text' || @@ -69,7 +99,12 @@ async function fetchOpenRouterModels(): Promise { const outputCost = parseFloat(model.pricing.completion); // Skip models with invalid pricing - if (isNaN(inputCost) || isNaN(outputCost)) { + if ( + isNaN(inputCost) || + isNaN(outputCost) || + inputCost === 0 || + outputCost === 0 + ) { console.warn(`⚠️ Skipping ${model.id} - invalid pricing data`); continue; } @@ -118,7 +153,7 @@ function generateOpenRouterModelFile(models: SupportedModel[]): string { model_id: "${model.model_id}", input_cost_per_token: ${model.input_cost_per_token}, output_cost_per_token: ${model.output_cost_per_token}, - provider: "OpenRouter" + provider: "${model.provider}", }`; }) .join(',\n'); diff --git a/packages/sdk/ts/src/supported-models/chat/anthropic.ts b/packages/sdk/ts/src/supported-models/chat/anthropic.ts index b669618e1..de039a1fc 100644 --- a/packages/sdk/ts/src/supported-models/chat/anthropic.ts +++ b/packages/sdk/ts/src/supported-models/chat/anthropic.ts @@ -8,6 +8,7 @@ export type AnthropicModel = | 'claude-3-7-sonnet-20250219' | 'claude-3-haiku-20240307' | 'claude-3-opus-20240229' + | 'claude-haiku-4-5-20251001' | 'claude-opus-4-1-20250805' | 'claude-opus-4-20250514' | 'claude-sonnet-4-20250514' @@ -50,6 +51,12 @@ export const AnthropicModels: SupportedModel[] = [ output_cost_per_token: 0.000075, provider: 'Anthropic', }, + { + model_id: 'claude-haiku-4-5-20251001', + input_cost_per_token: 0.000001, + output_cost_per_token: 0.000005, + provider: 'Anthropic', + }, { model_id: 'claude-opus-4-1-20250805', input_cost_per_token: 0.000015, diff --git a/packages/sdk/ts/src/supported-models/chat/gemini.ts b/packages/sdk/ts/src/supported-models/chat/gemini.ts index 4166da5b6..53197b7f5 100644 --- a/packages/sdk/ts/src/supported-models/chat/gemini.ts +++ b/packages/sdk/ts/src/supported-models/chat/gemini.ts @@ -15,41 +15,43 @@ export type GeminiModel = | 'gemini-2.0-flash-thinking-exp-01-21' | 'gemini-2.0-flash-thinking-exp-1219' | 'gemini-2.5-flash' + | 'gemini-2.5-flash-image' | 'gemini-2.5-flash-image-preview' | 'gemini-2.5-flash-lite' | 'gemini-2.5-flash-lite-preview-06-17' + | 'gemini-2.5-flash-lite-preview-09-2025' | 'gemini-2.5-flash-preview-05-20' + | 'gemini-2.5-flash-preview-09-2025' | 'gemini-2.5-flash-preview-tts' | 'gemini-2.5-pro' | 'gemini-2.5-pro-preview-03-25' | 'gemini-2.5-pro-preview-05-06' | 'gemini-2.5-pro-preview-06-05' - | 'gemini-2.5-pro-preview-tts' - | 'gemini-2.5-flash-image-preview'; + | 'gemini-2.5-pro-preview-tts'; export const GeminiModels: SupportedModel[] = [ { model_id: 'gemini-2.0-flash', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { model_id: 'gemini-2.0-flash-001', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { model_id: 'gemini-2.0-flash-exp', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { model_id: 'gemini-2.0-flash-exp-image-generation', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { @@ -78,26 +80,26 @@ export const GeminiModels: SupportedModel[] = [ }, { model_id: 'gemini-2.0-flash-preview-image-generation', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { model_id: 'gemini-2.0-flash-thinking-exp', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { model_id: 'gemini-2.0-flash-thinking-exp-01-21', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { model_id: 'gemini-2.0-flash-thinking-exp-1219', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'Gemini', }, { @@ -106,6 +108,12 @@ export const GeminiModels: SupportedModel[] = [ output_cost_per_token: 0.0000025, provider: 'Gemini', }, + { + model_id: 'gemini-2.5-flash-image', + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000025, + provider: 'Gemini', + }, { model_id: 'gemini-2.5-flash-image-preview', input_cost_per_token: 3e-7, @@ -124,12 +132,24 @@ export const GeminiModels: SupportedModel[] = [ output_cost_per_token: 4e-7, provider: 'Gemini', }, + { + model_id: 'gemini-2.5-flash-lite-preview-09-2025', + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, + provider: 'Gemini', + }, { model_id: 'gemini-2.5-flash-preview-05-20', input_cost_per_token: 3e-7, output_cost_per_token: 0.0000025, provider: 'Gemini', }, + { + model_id: 'gemini-2.5-flash-preview-09-2025', + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000025, + provider: 'Gemini', + }, { model_id: 'gemini-2.5-flash-preview-tts', input_cost_per_token: 3e-7, @@ -138,31 +158,31 @@ export const GeminiModels: SupportedModel[] = [ }, { model_id: 'gemini-2.5-pro', - input_cost_per_token: 0.0000025, + input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'Gemini', }, { model_id: 'gemini-2.5-pro-preview-03-25', - input_cost_per_token: 0.0000025, + input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'Gemini', }, { model_id: 'gemini-2.5-pro-preview-05-06', - input_cost_per_token: 0.0000025, + input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'Gemini', }, { model_id: 'gemini-2.5-pro-preview-06-05', - input_cost_per_token: 0.0000025, + input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'Gemini', }, { model_id: 'gemini-2.5-pro-preview-tts', - input_cost_per_token: 0.0000025, + input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'Gemini', }, diff --git a/packages/sdk/ts/src/supported-models/chat/openai.ts b/packages/sdk/ts/src/supported-models/chat/openai.ts index d79d451cd..937440197 100644 --- a/packages/sdk/ts/src/supported-models/chat/openai.ts +++ b/packages/sdk/ts/src/supported-models/chat/openai.ts @@ -5,6 +5,9 @@ export type OpenAIModel = | 'gpt-3.5-turbo' | 'gpt-3.5-turbo-0125' | 'gpt-3.5-turbo-1106' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-instruct' + | 'gpt-3.5-turbo-instruct-0914' | 'gpt-4' | 'gpt-4-0125-preview' | 'gpt-4-0613' @@ -24,15 +27,26 @@ export type OpenAIModel = | 'gpt-4o-2024-11-20' | 'gpt-4o-mini' | 'gpt-4o-mini-2024-07-18' + | 'gpt-4o-mini-search-preview' + | 'gpt-4o-mini-search-preview-2025-03-11' + | 'gpt-4o-search-preview' + | 'gpt-4o-search-preview-2025-03-11' | 'gpt-5' | 'gpt-5-2025-08-07' | 'gpt-5-chat-latest' + | 'gpt-5-codex' | 'gpt-5-mini' | 'gpt-5-mini-2025-08-07' | 'gpt-5-nano' | 'gpt-5-nano-2025-08-07' + | 'gpt-5-pro' + | 'gpt-5-pro-2025-10-06' + | 'gpt-5-search-api' + | 'gpt-5-search-api-2025-10-14' | 'o1' | 'o1-2024-12-17' + | 'o1-mini' + | 'o1-mini-2024-09-12' | 'o1-pro' | 'o1-pro-2025-03-19' | 'o3' @@ -63,6 +77,24 @@ export const OpenAIModels: SupportedModel[] = [ output_cost_per_token: 0.0000015, provider: 'OpenAI', }, + { + model_id: 'gpt-3.5-turbo-16k', + input_cost_per_token: 5e-7, + output_cost_per_token: 0.0000015, + provider: 'OpenAI', + }, + { + model_id: 'gpt-3.5-turbo-instruct', + input_cost_per_token: 0.0000015, + output_cost_per_token: 0.000002, + provider: 'OpenAI', + }, + { + model_id: 'gpt-3.5-turbo-instruct-0914', + input_cost_per_token: 0.0000015, + output_cost_per_token: 0.000002, + provider: 'OpenAI', + }, { model_id: 'gpt-4', input_cost_per_token: 4e-7, @@ -177,6 +209,30 @@ export const OpenAIModels: SupportedModel[] = [ output_cost_per_token: 6e-7, provider: 'OpenAI', }, + { + model_id: 'gpt-4o-mini-search-preview', + input_cost_per_token: 1.5e-7, + output_cost_per_token: 6e-7, + provider: 'OpenAI', + }, + { + model_id: 'gpt-4o-mini-search-preview-2025-03-11', + input_cost_per_token: 1.5e-7, + output_cost_per_token: 6e-7, + provider: 'OpenAI', + }, + { + model_id: 'gpt-4o-search-preview', + input_cost_per_token: 0.0000025, + output_cost_per_token: 0.00001, + provider: 'OpenAI', + }, + { + model_id: 'gpt-4o-search-preview-2025-03-11', + input_cost_per_token: 0.0000025, + output_cost_per_token: 0.00001, + provider: 'OpenAI', + }, { model_id: 'gpt-5', input_cost_per_token: 0.00000125, @@ -195,6 +251,12 @@ export const OpenAIModels: SupportedModel[] = [ output_cost_per_token: 0.00001, provider: 'OpenAI', }, + { + model_id: 'gpt-5-codex', + input_cost_per_token: 0.00000125, + output_cost_per_token: 0.00001, + provider: 'OpenAI', + }, { model_id: 'gpt-5-mini', input_cost_per_token: 2.5e-7, @@ -219,6 +281,30 @@ export const OpenAIModels: SupportedModel[] = [ output_cost_per_token: 4e-7, provider: 'OpenAI', }, + { + model_id: 'gpt-5-pro', + input_cost_per_token: 0.000015, + output_cost_per_token: 0.00012, + provider: 'OpenAI', + }, + { + model_id: 'gpt-5-pro-2025-10-06', + input_cost_per_token: 0.000015, + output_cost_per_token: 0.00012, + provider: 'OpenAI', + }, + { + model_id: 'gpt-5-search-api', + input_cost_per_token: 0.00000125, + output_cost_per_token: 0.00001, + provider: 'OpenAI', + }, + { + model_id: 'gpt-5-search-api-2025-10-14', + input_cost_per_token: 0.00000125, + output_cost_per_token: 0.00001, + provider: 'OpenAI', + }, { model_id: 'o1', input_cost_per_token: 0.000015, @@ -231,6 +317,18 @@ export const OpenAIModels: SupportedModel[] = [ output_cost_per_token: 0.00006, provider: 'OpenAI', }, + { + model_id: 'o1-mini', + input_cost_per_token: 0.000015, + output_cost_per_token: 0.00006, + provider: 'OpenAI', + }, + { + model_id: 'o1-mini-2024-09-12', + input_cost_per_token: 0.000015, + output_cost_per_token: 0.00006, + provider: 'OpenAI', + }, { model_id: 'o1-pro', input_cost_per_token: 0.000015, diff --git a/packages/sdk/ts/src/supported-models/chat/openrouter.ts b/packages/sdk/ts/src/supported-models/chat/openrouter.ts index 6da906b13..69b98053c 100644 --- a/packages/sdk/ts/src/supported-models/chat/openrouter.ts +++ b/packages/sdk/ts/src/supported-models/chat/openrouter.ts @@ -2,137 +2,104 @@ import { SupportedModel } from '../types'; // Union type of all valid OpenRouter model IDs export type OpenRouterModel = - | 'agentica-org/deepcoder-14b-preview' - | 'agentica-org/deepcoder-14b-preview:free' | 'ai21/jamba-large-1.7' | 'ai21/jamba-mini-1.7' | 'aion-labs/aion-1.0' | 'aion-labs/aion-1.0-mini' | 'aion-labs/aion-rp-llama-3.1-8b' | 'alfredpros/codellama-7b-instruct-solidity' + | 'alibaba/tongyi-deepresearch-30b-a3b' + | 'allenai/molmo-7b-d' + | 'allenai/olmo-2-0325-32b-instruct' | 'alpindale/goliath-120b' | 'amazon/nova-lite-v1' | 'amazon/nova-micro-v1' | 'amazon/nova-pro-v1' - | 'anthracite-org/magnum-v2-72b' | 'anthracite-org/magnum-v4-72b' | 'anthropic/claude-3-haiku' | 'anthropic/claude-3-opus' | 'anthropic/claude-3.5-haiku' - | 'anthropic/claude-3.5-haiku-20241022' | 'anthropic/claude-3.5-sonnet' | 'anthropic/claude-3.5-sonnet-20240620' | 'anthropic/claude-3.7-sonnet' | 'anthropic/claude-3.7-sonnet:thinking' + | 'anthropic/claude-haiku-4.5' | 'anthropic/claude-opus-4' | 'anthropic/claude-opus-4.1' | 'anthropic/claude-sonnet-4' | 'anthropic/claude-sonnet-4.5' + | 'arcee-ai/afm-4.5b' | 'arcee-ai/coder-large' | 'arcee-ai/maestro-reasoning' - | 'arcee-ai/spotlight' | 'arcee-ai/virtuoso-large' - | 'arliai/qwq-32b-arliai-rpr-v1' - | 'arliai/qwq-32b-arliai-rpr-v1:free' | 'baidu/ernie-4.5-21b-a3b' + | 'baidu/ernie-4.5-21b-a3b-thinking' | 'baidu/ernie-4.5-300b-a47b' | 'baidu/ernie-4.5-vl-28b-a3b' | 'baidu/ernie-4.5-vl-424b-a47b' | 'bytedance/ui-tars-1.5-7b' - | 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free' - | 'cognitivecomputations/dolphin-mixtral-8x22b' | 'cognitivecomputations/dolphin3.0-mistral-24b' - | 'cognitivecomputations/dolphin3.0-mistral-24b:free' - | 'cognitivecomputations/dolphin3.0-r1-mistral-24b' - | 'cognitivecomputations/dolphin3.0-r1-mistral-24b:free' - | 'cohere/command' | 'cohere/command-a' - | 'cohere/command-r' - | 'cohere/command-r-03-2024' | 'cohere/command-r-08-2024' - | 'cohere/command-r-plus' - | 'cohere/command-r-plus-04-2024' | 'cohere/command-r-plus-08-2024' | 'cohere/command-r7b-12-2024' + | 'deepcogito/cogito-v2-preview-deepseek-671b' + | 'deepcogito/cogito-v2-preview-llama-109b-moe' | 'deepseek/deepseek-chat' | 'deepseek/deepseek-chat-v3-0324' - | 'deepseek/deepseek-chat-v3-0324:free' | 'deepseek/deepseek-chat-v3.1' | 'deepseek/deepseek-prover-v2' | 'deepseek/deepseek-r1' | 'deepseek/deepseek-r1-0528' | 'deepseek/deepseek-r1-0528-qwen3-8b' - | 'deepseek/deepseek-r1-0528-qwen3-8b:free' - | 'deepseek/deepseek-r1-0528:free' | 'deepseek/deepseek-r1-distill-llama-70b' - | 'deepseek/deepseek-r1-distill-llama-70b:free' - | 'deepseek/deepseek-r1-distill-llama-8b' - | 'deepseek/deepseek-r1-distill-qwen-1.5b' | 'deepseek/deepseek-r1-distill-qwen-14b' - | 'deepseek/deepseek-r1-distill-qwen-14b:free' | 'deepseek/deepseek-r1-distill-qwen-32b' - | 'deepseek/deepseek-r1:free' - | 'deepseek/deepseek-v3.1-base' + | 'deepseek/deepseek-v3.1-terminus' + | 'deepseek/deepseek-v3.2-exp' | 'eleutherai/llemma_7b' | 'google/gemini-2.0-flash-001' - | 'google/gemini-2.0-flash-exp:free' | 'google/gemini-2.0-flash-lite-001' | 'google/gemini-2.5-flash' | 'google/gemini-2.5-flash-lite' | 'google/gemini-2.5-flash-lite-preview-06-17' + | 'google/gemini-2.5-flash-lite-preview-09-2025' + | 'google/gemini-2.5-flash-preview-09-2025' | 'google/gemini-2.5-pro' - | 'google/gemini-2.5-pro-exp-03-25' | 'google/gemini-2.5-pro-preview' | 'google/gemini-2.5-pro-preview-05-06' - | 'google/gemini-flash-1.5' - | 'google/gemini-flash-1.5-8b' - | 'google/gemini-pro-1.5' | 'google/gemma-2-27b-it' | 'google/gemma-2-9b-it' - | 'google/gemma-2-9b-it:free' | 'google/gemma-3-12b-it' - | 'google/gemma-3-12b-it:free' | 'google/gemma-3-27b-it' - | 'google/gemma-3-27b-it:free' | 'google/gemma-3-4b-it' - | 'google/gemma-3-4b-it:free' - | 'google/gemma-3n-e2b-it:free' | 'google/gemma-3n-e4b-it' - | 'google/gemma-3n-e4b-it:free' | 'gryphe/mythomax-l2-13b' | 'inception/mercury' | 'inception/mercury-coder' - | 'infermatic/mn-inferor-12b' | 'inflection/inflection-3-pi' | 'inflection/inflection-3-productivity' | 'liquid/lfm-3b' | 'liquid/lfm-7b' | 'mancer/weaver' + | 'meituan/longcat-flash-chat' | 'meta-llama/llama-3-70b-instruct' | 'meta-llama/llama-3-8b-instruct' | 'meta-llama/llama-3.1-405b' | 'meta-llama/llama-3.1-405b-instruct' - | 'meta-llama/llama-3.1-405b-instruct:free' | 'meta-llama/llama-3.1-70b-instruct' | 'meta-llama/llama-3.1-8b-instruct' | 'meta-llama/llama-3.2-11b-vision-instruct' - | 'meta-llama/llama-3.2-11b-vision-instruct:free' | 'meta-llama/llama-3.2-1b-instruct' | 'meta-llama/llama-3.2-3b-instruct' - | 'meta-llama/llama-3.2-3b-instruct:free' | 'meta-llama/llama-3.2-90b-vision-instruct' | 'meta-llama/llama-3.3-70b-instruct' - | 'meta-llama/llama-3.3-70b-instruct:free' - | 'meta-llama/llama-3.3-8b-instruct:free' | 'meta-llama/llama-4-maverick' - | 'meta-llama/llama-4-maverick:free' | 'meta-llama/llama-4-scout' - | 'meta-llama/llama-4-scout:free' | 'meta-llama/llama-guard-2-8b' | 'meta-llama/llama-guard-3-8b' | 'meta-llama/llama-guard-4-12b' | 'microsoft/mai-ds-r1' - | 'microsoft/mai-ds-r1:free' | 'microsoft/phi-3-medium-128k-instruct' | 'microsoft/phi-3-mini-128k-instruct' | 'microsoft/phi-3.5-mini-128k-instruct' @@ -140,14 +107,12 @@ export type OpenRouterModel = | 'microsoft/phi-4-multimodal-instruct' | 'microsoft/phi-4-reasoning-plus' | 'microsoft/wizardlm-2-8x22b' - | 'minimax/minimax-01' | 'minimax/minimax-m1' | 'mistralai/codestral-2501' | 'mistralai/codestral-2508' | 'mistralai/devstral-medium' | 'mistralai/devstral-small' | 'mistralai/devstral-small-2505' - | 'mistralai/devstral-small-2505:free' | 'mistralai/magistral-medium-2506' | 'mistralai/magistral-medium-2506:thinking' | 'mistralai/magistral-small-2506' @@ -155,50 +120,41 @@ export type OpenRouterModel = | 'mistralai/ministral-8b' | 'mistralai/mistral-7b-instruct' | 'mistralai/mistral-7b-instruct-v0.1' + | 'mistralai/mistral-7b-instruct-v0.2' | 'mistralai/mistral-7b-instruct-v0.3' - | 'mistralai/mistral-7b-instruct:free' | 'mistralai/mistral-large' | 'mistralai/mistral-large-2407' | 'mistralai/mistral-large-2411' | 'mistralai/mistral-medium-3' | 'mistralai/mistral-medium-3.1' | 'mistralai/mistral-nemo' - | 'mistralai/mistral-nemo:free' | 'mistralai/mistral-saba' | 'mistralai/mistral-small' | 'mistralai/mistral-small-24b-instruct-2501' - | 'mistralai/mistral-small-24b-instruct-2501:free' | 'mistralai/mistral-small-3.1-24b-instruct' - | 'mistralai/mistral-small-3.1-24b-instruct:free' | 'mistralai/mistral-small-3.2-24b-instruct' - | 'mistralai/mistral-small-3.2-24b-instruct:free' | 'mistralai/mistral-tiny' | 'mistralai/mixtral-8x22b-instruct' | 'mistralai/mixtral-8x7b-instruct' | 'mistralai/pixtral-12b' | 'mistralai/pixtral-large-2411' - | 'moonshotai/kimi-dev-72b:free' + | 'moonshotai/kimi-dev-72b' | 'moonshotai/kimi-k2' - | 'moonshotai/kimi-k2:free' - | 'moonshotai/kimi-vl-a3b-thinking' - | 'moonshotai/kimi-vl-a3b-thinking:free' + | 'moonshotai/kimi-k2-0905' | 'morph/morph-v3-fast' | 'morph/morph-v3-large' - | 'neversleep/llama-3-lumimaid-70b' | 'neversleep/llama-3.1-lumimaid-8b' | 'neversleep/noromaid-20b' - | 'nousresearch/deephermes-3-llama-3-8b-preview:free' + | 'nousresearch/deephermes-3-llama-3-8b-preview' | 'nousresearch/deephermes-3-mistral-24b-preview' | 'nousresearch/hermes-2-pro-llama-3-8b' | 'nousresearch/hermes-3-llama-3.1-405b' - | 'nousresearch/hermes-3-llama-3.1-70b' | 'nousresearch/hermes-4-405b' | 'nousresearch/hermes-4-70b' - | 'nousresearch/nous-hermes-2-mixtral-8x7b-dpo' | 'nvidia/llama-3.1-nemotron-70b-instruct' | 'nvidia/llama-3.1-nemotron-ultra-253b-v1' - | 'nvidia/llama-3.1-nemotron-ultra-253b-v1:free' - | 'nvidia/llama-3.3-nemotron-super-49b-v1' + | 'nvidia/llama-3.3-nemotron-super-49b-v1.5' + | 'nvidia/nemotron-nano-9b-v2' | 'openai/chatgpt-4o-latest' | 'openai/codex-mini' | 'openai/gpt-3.5-turbo' @@ -217,7 +173,6 @@ export type OpenRouterModel = | 'openai/gpt-4o-2024-05-13' | 'openai/gpt-4o-2024-08-06' | 'openai/gpt-4o-2024-11-20' - | 'openai/gpt-4o-audio-preview' | 'openai/gpt-4o-mini' | 'openai/gpt-4o-mini-2024-07-18' | 'openai/gpt-4o-mini-search-preview' @@ -225,117 +180,99 @@ export type OpenRouterModel = | 'openai/gpt-4o:extended' | 'openai/gpt-5' | 'openai/gpt-5-chat' + | 'openai/gpt-5-codex' | 'openai/gpt-5-mini' | 'openai/gpt-5-nano' + | 'openai/gpt-5-pro' | 'openai/gpt-oss-120b' | 'openai/gpt-oss-20b' - | 'openai/gpt-oss-20b:free' | 'openai/o1' | 'openai/o1-mini' | 'openai/o1-mini-2024-09-12' | 'openai/o1-pro' | 'openai/o3' + | 'openai/o3-deep-research' | 'openai/o3-mini' | 'openai/o3-mini-high' | 'openai/o3-pro' | 'openai/o4-mini' + | 'openai/o4-mini-deep-research' | 'openai/o4-mini-high' - | 'opengvlab/internvl3-14b' - | 'perplexity/r1-1776' + | 'opengvlab/internvl3-78b' | 'perplexity/sonar' | 'perplexity/sonar-deep-research' | 'perplexity/sonar-pro' | 'perplexity/sonar-reasoning' | 'perplexity/sonar-reasoning-pro' - | 'pygmalionai/mythalion-13b' - | 'qwen/qwen-2-72b-instruct' | 'qwen/qwen-2.5-72b-instruct' - | 'qwen/qwen-2.5-72b-instruct:free' | 'qwen/qwen-2.5-7b-instruct' - | 'qwen/qwen-2.5-coder-32b-instruct' - | 'qwen/qwen-2.5-coder-32b-instruct:free' | 'qwen/qwen-2.5-vl-7b-instruct' | 'qwen/qwen-max' | 'qwen/qwen-plus' + | 'qwen/qwen-plus-2025-07-28' + | 'qwen/qwen-plus-2025-07-28:thinking' | 'qwen/qwen-turbo' | 'qwen/qwen-vl-max' | 'qwen/qwen-vl-plus' + | 'qwen/qwen2.5-coder-7b-instruct' | 'qwen/qwen2.5-vl-32b-instruct' - | 'qwen/qwen2.5-vl-32b-instruct:free' | 'qwen/qwen2.5-vl-72b-instruct' - | 'qwen/qwen2.5-vl-72b-instruct:free' | 'qwen/qwen3-14b' - | 'qwen/qwen3-14b:free' | 'qwen/qwen3-235b-a22b' | 'qwen/qwen3-235b-a22b-2507' | 'qwen/qwen3-235b-a22b-thinking-2507' - | 'qwen/qwen3-235b-a22b:free' | 'qwen/qwen3-30b-a3b' | 'qwen/qwen3-30b-a3b-instruct-2507' - | 'qwen/qwen3-30b-a3b:free' + | 'qwen/qwen3-30b-a3b-thinking-2507' | 'qwen/qwen3-32b' - | 'qwen/qwen3-4b:free' | 'qwen/qwen3-8b' - | 'qwen/qwen3-8b:free' | 'qwen/qwen3-coder' - | 'qwen/qwen3-coder:free' + | 'qwen/qwen3-coder-30b-a3b-instruct' + | 'qwen/qwen3-coder-flash' + | 'qwen/qwen3-coder-plus' + | 'qwen/qwen3-max' + | 'qwen/qwen3-next-80b-a3b-instruct' + | 'qwen/qwen3-vl-235b-a22b-instruct' + | 'qwen/qwen3-vl-235b-a22b-thinking' + | 'qwen/qwen3-vl-30b-a3b-instruct' + | 'qwen/qwen3-vl-30b-a3b-thinking' + | 'qwen/qwen3-vl-8b-instruct' + | 'qwen/qwen3-vl-8b-thinking' | 'qwen/qwq-32b' - | 'qwen/qwq-32b-preview' - | 'qwen/qwq-32b:free' | 'raifle/sorcererlm-8x22b' - | 'rekaai/reka-flash-3:free' | 'sao10k/l3-euryale-70b' | 'sao10k/l3-lunaris-8b' + | 'sao10k/l3.1-70b-hanami-x1' | 'sao10k/l3.1-euryale-70b' | 'sao10k/l3.3-euryale-70b' - | 'sarvamai/sarvam-m:free' - | 'scb10x/llama3.1-typhoon2-70b-instruct' | 'shisa-ai/shisa-v2-llama3.3-70b' - | 'shisa-ai/shisa-v2-llama3.3-70b:free' - | 'sophosympatheia/midnight-rose-70b' + | 'stepfun-ai/step3' | 'switchpoint/router' | 'tencent/hunyuan-a13b-instruct' - | 'tencent/hunyuan-a13b-instruct:free' | 'thedrummer/anubis-70b-v1.1' - | 'thedrummer/anubis-pro-105b-v1' + | 'thedrummer/cydonia-24b-v4.1' | 'thedrummer/rocinante-12b' | 'thedrummer/skyfall-36b-v2' | 'thedrummer/unslopnemo-12b' - | 'thudm/glm-4-32b' | 'thudm/glm-4.1v-9b-thinking' | 'thudm/glm-z1-32b' | 'tngtech/deepseek-r1t-chimera' - | 'tngtech/deepseek-r1t-chimera:free' - | 'tngtech/deepseek-r1t2-chimera:free' + | 'tngtech/deepseek-r1t2-chimera' | 'undi95/remm-slerp-l2-13b' - | 'x-ai/grok-2-1212' - | 'x-ai/grok-2-vision-1212' | 'x-ai/grok-3' | 'x-ai/grok-3-beta' | 'x-ai/grok-3-mini' | 'x-ai/grok-3-mini-beta' | 'x-ai/grok-4' + | 'x-ai/grok-4-fast' | 'x-ai/grok-code-fast-1' - | 'x-ai/grok-vision-beta' | 'z-ai/glm-4-32b' | 'z-ai/glm-4.5' | 'z-ai/glm-4.5-air' - | 'z-ai/glm-4.5-air:free' - | 'z-ai/glm-4.5v'; + | 'z-ai/glm-4.5v' + | 'z-ai/glm-4.6'; export const OpenRouterModels: SupportedModel[] = [ - { - model_id: 'agentica-org/deepcoder-14b-preview', - input_cost_per_token: 1.5e-8, - output_cost_per_token: 1.5e-8, - provider: 'OpenRouter', - }, - { - model_id: 'agentica-org/deepcoder-14b-preview:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'ai21/jamba-large-1.7', input_cost_per_token: 0.000002, @@ -368,8 +305,26 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'alfredpros/codellama-7b-instruct-solidity', - input_cost_per_token: 7e-7, - output_cost_per_token: 0.0000011, + input_cost_per_token: 8e-7, + output_cost_per_token: 0.0000012, + provider: 'OpenRouter', + }, + { + model_id: 'alibaba/tongyi-deepresearch-30b-a3b', + input_cost_per_token: 9e-8, + output_cost_per_token: 4e-7, + provider: 'OpenRouter', + }, + { + model_id: 'allenai/molmo-7b-d', + input_cost_per_token: 1e-7, + output_cost_per_token: 2e-7, + provider: 'OpenRouter', + }, + { + model_id: 'allenai/olmo-2-0325-32b-instruct', + input_cost_per_token: 2e-7, + output_cost_per_token: 3.5e-7, provider: 'OpenRouter', }, { @@ -396,15 +351,9 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.0000032, provider: 'OpenRouter', }, - { - model_id: 'anthracite-org/magnum-v2-72b', - input_cost_per_token: 0.000003, - output_cost_per_token: 0.000003, - provider: 'OpenRouter', - }, { model_id: 'anthracite-org/magnum-v4-72b', - input_cost_per_token: 0.000002, + input_cost_per_token: 0.0000025, output_cost_per_token: 0.000005, provider: 'OpenRouter', }, @@ -426,12 +375,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000004, provider: 'OpenRouter', }, - { - model_id: 'anthropic/claude-3.5-haiku-20241022', - input_cost_per_token: 8e-7, - output_cost_per_token: 0.000004, - provider: 'OpenRouter', - }, { model_id: 'anthropic/claude-3.5-sonnet', input_cost_per_token: 0.000003, @@ -456,6 +399,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000015, provider: 'OpenRouter', }, + { + model_id: 'anthropic/claude-haiku-4.5', + input_cost_per_token: 0.000001, + output_cost_per_token: 0.000005, + provider: 'OpenRouter', + }, { model_id: 'anthropic/claude-opus-4', input_cost_per_token: 0.000015, @@ -480,6 +429,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000015, provider: 'OpenRouter', }, + { + model_id: 'arcee-ai/afm-4.5b', + input_cost_per_token: 4.8e-8, + output_cost_per_token: 1.5e-7, + provider: 'OpenRouter', + }, { model_id: 'arcee-ai/coder-large', input_cost_per_token: 5e-7, @@ -492,12 +447,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.0000033, provider: 'OpenRouter', }, - { - model_id: 'arcee-ai/spotlight', - input_cost_per_token: 1.8e-7, - output_cost_per_token: 1.8e-7, - provider: 'OpenRouter', - }, { model_id: 'arcee-ai/virtuoso-large', input_cost_per_token: 7.5e-7, @@ -505,19 +454,13 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'arliai/qwq-32b-arliai-rpr-v1', - input_cost_per_token: 1e-8, - output_cost_per_token: 4.00032e-8, - provider: 'OpenRouter', - }, - { - model_id: 'arliai/qwq-32b-arliai-rpr-v1:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'baidu/ernie-4.5-21b-a3b', + input_cost_per_token: 7e-8, + output_cost_per_token: 2.8e-7, provider: 'OpenRouter', }, { - model_id: 'baidu/ernie-4.5-21b-a3b', + model_id: 'baidu/ernie-4.5-21b-a3b-thinking', input_cost_per_token: 7e-8, output_cost_per_token: 2.8e-7, provider: 'OpenRouter', @@ -546,64 +489,16 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 2e-7, provider: 'OpenRouter', }, - { - model_id: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'cognitivecomputations/dolphin-mixtral-8x22b', - input_cost_per_token: 9e-7, - output_cost_per_token: 9e-7, - provider: 'OpenRouter', - }, { model_id: 'cognitivecomputations/dolphin3.0-mistral-24b', - input_cost_per_token: 3.7022e-8, - output_cost_per_token: 1.4816e-7, - provider: 'OpenRouter', - }, - { - model_id: 'cognitivecomputations/dolphin3.0-mistral-24b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'cognitivecomputations/dolphin3.0-r1-mistral-24b', - input_cost_per_token: 1e-8, - output_cost_per_token: 3.40768e-8, - provider: 'OpenRouter', - }, - { - model_id: 'cognitivecomputations/dolphin3.0-r1-mistral-24b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'cohere/command', - input_cost_per_token: 0.000001, - output_cost_per_token: 0.000002, + input_cost_per_token: 4e-8, + output_cost_per_token: 1.7e-7, provider: 'OpenRouter', }, { model_id: 'cohere/command-a', - input_cost_per_token: 0.000002, - output_cost_per_token: 0.000008, - provider: 'OpenRouter', - }, - { - model_id: 'cohere/command-r', - input_cost_per_token: 5e-7, - output_cost_per_token: 0.0000015, - provider: 'OpenRouter', - }, - { - model_id: 'cohere/command-r-03-2024', - input_cost_per_token: 5e-7, - output_cost_per_token: 0.0000015, + input_cost_per_token: 0.0000025, + output_cost_per_token: 0.00001, provider: 'OpenRouter', }, { @@ -612,18 +507,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 6e-7, provider: 'OpenRouter', }, - { - model_id: 'cohere/command-r-plus', - input_cost_per_token: 0.000003, - output_cost_per_token: 0.000015, - provider: 'OpenRouter', - }, - { - model_id: 'cohere/command-r-plus-04-2024', - input_cost_per_token: 0.000003, - output_cost_per_token: 0.000015, - provider: 'OpenRouter', - }, { model_id: 'cohere/command-r-plus-08-2024', input_cost_per_token: 0.0000025, @@ -637,21 +520,27 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'deepseek/deepseek-chat', - input_cost_per_token: 1.999188e-7, - output_cost_per_token: 8.00064e-7, + model_id: 'deepcogito/cogito-v2-preview-deepseek-671b', + input_cost_per_token: 0.00000125, + output_cost_per_token: 0.00000125, provider: 'OpenRouter', }, { - model_id: 'deepseek/deepseek-chat-v3-0324', - input_cost_per_token: 1.999188e-7, - output_cost_per_token: 8.00064e-7, + model_id: 'deepcogito/cogito-v2-preview-llama-109b-moe', + input_cost_per_token: 1.8e-7, + output_cost_per_token: 5.9e-7, provider: 'OpenRouter', }, { - model_id: 'deepseek/deepseek-chat-v3-0324:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'deepseek/deepseek-chat', + input_cost_per_token: 3e-7, + output_cost_per_token: 8.5e-7, + provider: 'OpenRouter', + }, + { + model_id: 'deepseek/deepseek-chat-v3-0324', + input_cost_per_token: 2.4e-7, + output_cost_per_token: 8.4e-7, provider: 'OpenRouter', }, { @@ -674,50 +563,20 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'deepseek/deepseek-r1-0528', - input_cost_per_token: 1.999188e-7, - output_cost_per_token: 8.00064e-7, + input_cost_per_token: 4e-7, + output_cost_per_token: 0.00000175, provider: 'OpenRouter', }, { model_id: 'deepseek/deepseek-r1-0528-qwen3-8b', - input_cost_per_token: 1e-8, - output_cost_per_token: 2e-8, - provider: 'OpenRouter', - }, - { - model_id: 'deepseek/deepseek-r1-0528-qwen3-8b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'deepseek/deepseek-r1-0528:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 3e-8, + output_cost_per_token: 1.1e-7, provider: 'OpenRouter', }, { model_id: 'deepseek/deepseek-r1-distill-llama-70b', - input_cost_per_token: 2.59154e-8, - output_cost_per_token: 1.03712e-7, - provider: 'OpenRouter', - }, - { - model_id: 'deepseek/deepseek-r1-distill-llama-70b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'deepseek/deepseek-r1-distill-llama-8b', - input_cost_per_token: 4e-8, - output_cost_per_token: 4e-8, - provider: 'OpenRouter', - }, - { - model_id: 'deepseek/deepseek-r1-distill-qwen-1.5b', - input_cost_per_token: 1.8e-7, - output_cost_per_token: 1.8e-7, + input_cost_per_token: 3e-8, + output_cost_per_token: 1.3e-7, provider: 'OpenRouter', }, { @@ -726,28 +585,22 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 1.5e-7, provider: 'OpenRouter', }, - { - model_id: 'deepseek/deepseek-r1-distill-qwen-14b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'deepseek/deepseek-r1-distill-qwen-32b', - input_cost_per_token: 7.5e-8, - output_cost_per_token: 1.5e-7, + input_cost_per_token: 2.7e-7, + output_cost_per_token: 2.7e-7, provider: 'OpenRouter', }, { - model_id: 'deepseek/deepseek-r1:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'deepseek/deepseek-v3.1-terminus', + input_cost_per_token: 2.3e-7, + output_cost_per_token: 9e-7, provider: 'OpenRouter', }, { - model_id: 'deepseek/deepseek-v3.1-base', - input_cost_per_token: 2e-7, - output_cost_per_token: 8e-7, + model_id: 'deepseek/deepseek-v3.2-exp', + input_cost_per_token: 2.7e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { @@ -762,12 +615,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 4e-7, provider: 'OpenRouter', }, - { - model_id: 'google/gemini-2.0-flash-exp:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'google/gemini-2.0-flash-lite-001', input_cost_per_token: 7.5e-8, @@ -793,45 +640,33 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'google/gemini-2.5-pro', - input_cost_per_token: 0.00000125, - output_cost_per_token: 0.00001, + model_id: 'google/gemini-2.5-flash-lite-preview-09-2025', + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { - model_id: 'google/gemini-2.5-pro-exp-03-25', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'google/gemini-2.5-flash-preview-09-2025', + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000025, provider: 'OpenRouter', }, { - model_id: 'google/gemini-2.5-pro-preview', + model_id: 'google/gemini-2.5-pro', input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'OpenRouter', }, { - model_id: 'google/gemini-2.5-pro-preview-05-06', + model_id: 'google/gemini-2.5-pro-preview', input_cost_per_token: 0.00000125, output_cost_per_token: 0.00001, provider: 'OpenRouter', }, { - model_id: 'google/gemini-flash-1.5', - input_cost_per_token: 7.5e-8, - output_cost_per_token: 3e-7, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemini-flash-1.5-8b', - input_cost_per_token: 3.75e-8, - output_cost_per_token: 1.5e-7, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemini-pro-1.5', + model_id: 'google/gemini-2.5-pro-preview-05-06', input_cost_per_token: 0.00000125, - output_cost_per_token: 0.000005, + output_cost_per_token: 0.00001, provider: 'OpenRouter', }, { @@ -843,55 +678,25 @@ export const OpenRouterModels: SupportedModel[] = [ { model_id: 'google/gemma-2-9b-it', input_cost_per_token: 1e-8, - output_cost_per_token: 1.00008e-8, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemma-2-9b-it:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + output_cost_per_token: 3e-8, provider: 'OpenRouter', }, { model_id: 'google/gemma-3-12b-it', - input_cost_per_token: 4.81286e-8, - output_cost_per_token: 1.92608e-7, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemma-3-12b-it:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 3e-8, + output_cost_per_token: 1e-7, provider: 'OpenRouter', }, { model_id: 'google/gemma-3-27b-it', - input_cost_per_token: 6.66396e-8, - output_cost_per_token: 2.66688e-7, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemma-3-27b-it:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 9e-8, + output_cost_per_token: 1.6e-7, provider: 'OpenRouter', }, { model_id: 'google/gemma-3-4b-it', - input_cost_per_token: 2e-8, - output_cost_per_token: 4e-8, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemma-3-4b-it:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'google/gemma-3n-e2b-it:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 1.703012e-8, + output_cost_per_token: 6.81536e-8, provider: 'OpenRouter', }, { @@ -900,16 +705,10 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 4e-8, provider: 'OpenRouter', }, - { - model_id: 'google/gemma-3n-e4b-it:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'gryphe/mythomax-l2-13b', - input_cost_per_token: 6e-8, - output_cost_per_token: 6e-8, + input_cost_per_token: 5e-8, + output_cost_per_token: 9e-8, provider: 'OpenRouter', }, { @@ -924,12 +723,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000001, provider: 'OpenRouter', }, - { - model_id: 'infermatic/mn-inferor-12b', - input_cost_per_token: 6e-7, - output_cost_per_token: 0.000001, - provider: 'OpenRouter', - }, { model_id: 'inflection/inflection-3-pi', input_cost_per_token: 0.0000025, @@ -960,6 +753,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000001125, provider: 'OpenRouter', }, + { + model_id: 'meituan/longcat-flash-chat', + input_cost_per_token: 1.5e-7, + output_cost_per_token: 7.5e-7, + provider: 'OpenRouter', + }, { model_id: 'meta-llama/llama-3-70b-instruct', input_cost_per_token: 3e-7, @@ -974,8 +773,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'meta-llama/llama-3.1-405b', - input_cost_per_token: 0.000002, - output_cost_per_token: 0.000002, + input_cost_per_token: 0.000004, + output_cost_per_token: 0.000004, provider: 'OpenRouter', }, { @@ -984,22 +783,16 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 8e-7, provider: 'OpenRouter', }, - { - model_id: 'meta-llama/llama-3.1-405b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'meta-llama/llama-3.1-70b-instruct', - input_cost_per_token: 1e-7, - output_cost_per_token: 2.8e-7, + input_cost_per_token: 4e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { model_id: 'meta-llama/llama-3.1-8b-instruct', - input_cost_per_token: 1.5e-8, - output_cost_per_token: 2e-8, + input_cost_per_token: 2e-8, + output_cost_per_token: 3e-8, provider: 'OpenRouter', }, { @@ -1008,12 +801,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 4.9e-8, provider: 'OpenRouter', }, - { - model_id: 'meta-llama/llama-3.2-11b-vision-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'meta-llama/llama-3.2-1b-instruct', input_cost_per_token: 5e-9, @@ -1022,38 +809,20 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'meta-llama/llama-3.2-3b-instruct', - input_cost_per_token: 3e-9, - output_cost_per_token: 6e-9, - provider: 'OpenRouter', - }, - { - model_id: 'meta-llama/llama-3.2-3b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 2e-8, + output_cost_per_token: 2e-8, provider: 'OpenRouter', }, { model_id: 'meta-llama/llama-3.2-90b-vision-instruct', - input_cost_per_token: 0.0000012, - output_cost_per_token: 0.0000012, + input_cost_per_token: 3.5e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { model_id: 'meta-llama/llama-3.3-70b-instruct', - input_cost_per_token: 3.8e-8, - output_cost_per_token: 1.2e-7, - provider: 'OpenRouter', - }, - { - model_id: 'meta-llama/llama-3.3-70b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'meta-llama/llama-3.3-8b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 1.3e-7, + output_cost_per_token: 3.8e-7, provider: 'OpenRouter', }, { @@ -1062,24 +831,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 6e-7, provider: 'OpenRouter', }, - { - model_id: 'meta-llama/llama-4-maverick:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'meta-llama/llama-4-scout', input_cost_per_token: 8e-8, output_cost_per_token: 3e-7, provider: 'OpenRouter', }, - { - model_id: 'meta-llama/llama-4-scout:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'meta-llama/llama-guard-2-8b', input_cost_per_token: 2e-7, @@ -1100,14 +857,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'microsoft/mai-ds-r1', - input_cost_per_token: 1.999188e-7, - output_cost_per_token: 8.00064e-7, - provider: 'OpenRouter', - }, - { - model_id: 'microsoft/mai-ds-r1:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { @@ -1152,16 +903,10 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 4.8e-7, provider: 'OpenRouter', }, - { - model_id: 'minimax/minimax-01', - input_cost_per_token: 2e-7, - output_cost_per_token: 0.0000011, - provider: 'OpenRouter', - }, { model_id: 'minimax/minimax-m1', - input_cost_per_token: 3e-7, - output_cost_per_token: 0.00000165, + input_cost_per_token: 4e-7, + output_cost_per_token: 0.0000022, provider: 'OpenRouter', }, { @@ -1190,14 +935,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'mistralai/devstral-small-2505', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, - provider: 'OpenRouter', - }, - { - model_id: 'mistralai/devstral-small-2505:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 5e-8, + output_cost_per_token: 2.2e-7, provider: 'OpenRouter', }, { @@ -1243,15 +982,15 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'mistralai/mistral-7b-instruct-v0.3', - input_cost_per_token: 2.8e-8, - output_cost_per_token: 5.4e-8, + model_id: 'mistralai/mistral-7b-instruct-v0.2', + input_cost_per_token: 2e-7, + output_cost_per_token: 2e-7, provider: 'OpenRouter', }, { - model_id: 'mistralai/mistral-7b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'mistralai/mistral-7b-instruct-v0.3', + input_cost_per_token: 2.8e-8, + output_cost_per_token: 5.4e-8, provider: 'OpenRouter', }, { @@ -1286,14 +1025,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'mistralai/mistral-nemo', - input_cost_per_token: 7.5e-9, - output_cost_per_token: 5e-8, - provider: 'OpenRouter', - }, - { - model_id: 'mistralai/mistral-nemo:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 2e-8, + output_cost_per_token: 4e-8, provider: 'OpenRouter', }, { @@ -1310,38 +1043,20 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'mistralai/mistral-small-24b-instruct-2501', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, - provider: 'OpenRouter', - }, - { - model_id: 'mistralai/mistral-small-24b-instruct-2501:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 5e-8, + output_cost_per_token: 8e-8, provider: 'OpenRouter', }, { model_id: 'mistralai/mistral-small-3.1-24b-instruct', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, - provider: 'OpenRouter', - }, - { - model_id: 'mistralai/mistral-small-3.1-24b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'mistralai/mistral-small-3.2-24b-instruct', input_cost_per_token: 5e-8, output_cost_per_token: 1e-7, provider: 'OpenRouter', }, { - model_id: 'mistralai/mistral-small-3.2-24b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'mistralai/mistral-small-3.2-24b-instruct', + input_cost_per_token: 6e-8, + output_cost_per_token: 1.8e-7, provider: 'OpenRouter', }, { @@ -1358,8 +1073,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'mistralai/mixtral-8x7b-instruct', - input_cost_per_token: 8e-8, - output_cost_per_token: 2.4e-7, + input_cost_per_token: 5.4e-7, + output_cost_per_token: 5.4e-7, provider: 'OpenRouter', }, { @@ -1375,39 +1090,27 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'moonshotai/kimi-dev-72b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'moonshotai/kimi-k2', - input_cost_per_token: 1.4e-7, - output_cost_per_token: 0.00000249, - provider: 'OpenRouter', - }, - { - model_id: 'moonshotai/kimi-k2:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'moonshotai/kimi-dev-72b', + input_cost_per_token: 2.9e-7, + output_cost_per_token: 0.00000115, provider: 'OpenRouter', }, { - model_id: 'moonshotai/kimi-vl-a3b-thinking', - input_cost_per_token: 2.498985e-8, - output_cost_per_token: 1.00008e-7, + model_id: 'moonshotai/kimi-k2', + input_cost_per_token: 1.4e-7, + output_cost_per_token: 0.00000249, provider: 'OpenRouter', }, { - model_id: 'moonshotai/kimi-vl-a3b-thinking:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'moonshotai/kimi-k2-0905', + input_cost_per_token: 3.9e-7, + output_cost_per_token: 0.0000019, provider: 'OpenRouter', }, { model_id: 'morph/morph-v3-fast', - input_cost_per_token: 9e-7, - output_cost_per_token: 0.0000019, + input_cost_per_token: 8e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { @@ -1416,12 +1119,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.0000019, provider: 'OpenRouter', }, - { - model_id: 'neversleep/llama-3-lumimaid-70b', - input_cost_per_token: 0.000004, - output_cost_per_token: 0.000006, - provider: 'OpenRouter', - }, { model_id: 'neversleep/llama-3.1-lumimaid-8b', input_cost_per_token: 9e-8, @@ -1435,59 +1132,47 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'nousresearch/deephermes-3-llama-3-8b-preview:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'nousresearch/deephermes-3-llama-3-8b-preview', + input_cost_per_token: 3e-8, + output_cost_per_token: 1.1e-7, provider: 'OpenRouter', }, { model_id: 'nousresearch/deephermes-3-mistral-24b-preview', - input_cost_per_token: 9.329544e-8, - output_cost_per_token: 3.733632e-7, + input_cost_per_token: 1.5e-7, + output_cost_per_token: 5.9e-7, provider: 'OpenRouter', }, { model_id: 'nousresearch/hermes-2-pro-llama-3-8b', input_cost_per_token: 2.5e-8, - output_cost_per_token: 4e-8, + output_cost_per_token: 8e-8, provider: 'OpenRouter', }, { model_id: 'nousresearch/hermes-3-llama-3.1-405b', - input_cost_per_token: 7e-7, - output_cost_per_token: 8e-7, - provider: 'OpenRouter', - }, - { - model_id: 'nousresearch/hermes-3-llama-3.1-70b', - input_cost_per_token: 1e-7, - output_cost_per_token: 2.8e-7, + input_cost_per_token: 0.000001, + output_cost_per_token: 0.000001, provider: 'OpenRouter', }, { model_id: 'nousresearch/hermes-4-405b', - input_cost_per_token: 0.000001, - output_cost_per_token: 0.000003, + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { model_id: 'nousresearch/hermes-4-70b', - input_cost_per_token: 1.3e-7, - output_cost_per_token: 4e-7, + input_cost_per_token: 1.1e-7, + output_cost_per_token: 3.8e-7, provider: 'OpenRouter', }, { - model_id: 'nousresearch/nous-hermes-2-mixtral-8x7b-dpo', + model_id: 'nvidia/llama-3.1-nemotron-70b-instruct', input_cost_per_token: 6e-7, output_cost_per_token: 6e-7, provider: 'OpenRouter', }, - { - model_id: 'nvidia/llama-3.1-nemotron-70b-instruct', - input_cost_per_token: 1.2e-7, - output_cost_per_token: 3e-7, - provider: 'OpenRouter', - }, { model_id: 'nvidia/llama-3.1-nemotron-ultra-253b-v1', input_cost_per_token: 6e-7, @@ -1495,15 +1180,15 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'nvidia/llama-3.1-nemotron-ultra-253b-v1:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'nvidia/llama-3.3-nemotron-super-49b-v1.5', + input_cost_per_token: 1e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { - model_id: 'nvidia/llama-3.3-nemotron-super-49b-v1', - input_cost_per_token: 1.3e-7, - output_cost_per_token: 4e-7, + model_id: 'nvidia/nemotron-nano-9b-v2', + input_cost_per_token: 4e-8, + output_cost_per_token: 1.6e-7, provider: 'OpenRouter', }, { @@ -1614,12 +1299,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.00001, provider: 'OpenRouter', }, - { - model_id: 'openai/gpt-4o-audio-preview', - input_cost_per_token: 0.0000025, - output_cost_per_token: 0.00001, - provider: 'OpenRouter', - }, { model_id: 'openai/gpt-4o-mini', input_cost_per_token: 1.5e-7, @@ -1662,6 +1341,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.00001, provider: 'OpenRouter', }, + { + model_id: 'openai/gpt-5-codex', + input_cost_per_token: 0.00000125, + output_cost_per_token: 0.00001, + provider: 'OpenRouter', + }, { model_id: 'openai/gpt-5-mini', input_cost_per_token: 2.5e-7, @@ -1675,21 +1360,21 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'openai/gpt-oss-120b', - input_cost_per_token: 7.2e-8, - output_cost_per_token: 2.8e-7, + model_id: 'openai/gpt-5-pro', + input_cost_per_token: 0.000015, + output_cost_per_token: 0.00012, provider: 'OpenRouter', }, { - model_id: 'openai/gpt-oss-20b', + model_id: 'openai/gpt-oss-120b', input_cost_per_token: 4e-8, - output_cost_per_token: 1.5e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { - model_id: 'openai/gpt-oss-20b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'openai/gpt-oss-20b', + input_cost_per_token: 3e-8, + output_cost_per_token: 1.4e-7, provider: 'OpenRouter', }, { @@ -1722,6 +1407,12 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000008, provider: 'OpenRouter', }, + { + model_id: 'openai/o3-deep-research', + input_cost_per_token: 0.00001, + output_cost_per_token: 0.00004, + provider: 'OpenRouter', + }, { model_id: 'openai/o3-mini', input_cost_per_token: 0.0000011, @@ -1747,21 +1438,21 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'openai/o4-mini-high', - input_cost_per_token: 0.0000011, - output_cost_per_token: 0.0000044, + model_id: 'openai/o4-mini-deep-research', + input_cost_per_token: 0.000002, + output_cost_per_token: 0.000008, provider: 'OpenRouter', }, { - model_id: 'opengvlab/internvl3-14b', - input_cost_per_token: 2e-7, - output_cost_per_token: 4e-7, + model_id: 'openai/o4-mini-high', + input_cost_per_token: 0.0000011, + output_cost_per_token: 0.0000044, provider: 'OpenRouter', }, { - model_id: 'perplexity/r1-1776', - input_cost_per_token: 0.000002, - output_cost_per_token: 0.000008, + model_id: 'opengvlab/internvl3-78b', + input_cost_per_token: 7e-8, + output_cost_per_token: 2.6e-7, provider: 'OpenRouter', }, { @@ -1794,28 +1485,10 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000008, provider: 'OpenRouter', }, - { - model_id: 'pygmalionai/mythalion-13b', - input_cost_per_token: 7e-7, - output_cost_per_token: 0.0000011, - provider: 'OpenRouter', - }, - { - model_id: 'qwen/qwen-2-72b-instruct', - input_cost_per_token: 9e-7, - output_cost_per_token: 9e-7, - provider: 'OpenRouter', - }, { model_id: 'qwen/qwen-2.5-72b-instruct', - input_cost_per_token: 5.18308e-8, - output_cost_per_token: 2.07424e-7, - provider: 'OpenRouter', - }, - { - model_id: 'qwen/qwen-2.5-72b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 7e-8, + output_cost_per_token: 2.6e-7, provider: 'OpenRouter', }, { @@ -1824,18 +1497,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 1e-7, provider: 'OpenRouter', }, - { - model_id: 'qwen/qwen-2.5-coder-32b-instruct', - input_cost_per_token: 4.99797e-8, - output_cost_per_token: 2.00016e-7, - provider: 'OpenRouter', - }, - { - model_id: 'qwen/qwen-2.5-coder-32b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'qwen/qwen-2.5-vl-7b-instruct', input_cost_per_token: 2e-7, @@ -1854,6 +1515,18 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, + { + model_id: 'qwen/qwen-plus-2025-07-28', + input_cost_per_token: 4e-7, + output_cost_per_token: 0.0000012, + provider: 'OpenRouter', + }, + { + model_id: 'qwen/qwen-plus-2025-07-28:thinking', + input_cost_per_token: 4e-7, + output_cost_per_token: 0.000004, + provider: 'OpenRouter', + }, { model_id: 'qwen/qwen-turbo', input_cost_per_token: 5e-8, @@ -1873,147 +1546,159 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'qwen/qwen2.5-vl-32b-instruct', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, + model_id: 'qwen/qwen2.5-coder-7b-instruct', + input_cost_per_token: 3e-8, + output_cost_per_token: 9e-8, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen2.5-vl-32b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen2.5-vl-32b-instruct', + input_cost_per_token: 5e-8, + output_cost_per_token: 2.2e-7, provider: 'OpenRouter', }, { model_id: 'qwen/qwen2.5-vl-72b-instruct', - input_cost_per_token: 9.99594e-8, - output_cost_per_token: 4.00032e-7, + input_cost_per_token: 8e-8, + output_cost_per_token: 3.3e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen2.5-vl-72b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-14b', + input_cost_per_token: 5e-8, + output_cost_per_token: 2.2e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-14b', - input_cost_per_token: 6e-8, - output_cost_per_token: 2.4e-7, + model_id: 'qwen/qwen3-235b-a22b', + input_cost_per_token: 1.8e-7, + output_cost_per_token: 5.4e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-14b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-235b-a22b-2507', + input_cost_per_token: 8e-8, + output_cost_per_token: 5.5e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-235b-a22b', - input_cost_per_token: 1.3e-7, + model_id: 'qwen/qwen3-235b-a22b-thinking-2507', + input_cost_per_token: 1.1e-7, output_cost_per_token: 6e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-235b-a22b-2507', - input_cost_per_token: 7.7968332e-8, - output_cost_per_token: 3.1202496e-7, + model_id: 'qwen/qwen3-30b-a3b', + input_cost_per_token: 6e-8, + output_cost_per_token: 2.2e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-235b-a22b-thinking-2507', - input_cost_per_token: 7.7968332e-8, - output_cost_per_token: 3.1202496e-7, + model_id: 'qwen/qwen3-30b-a3b-instruct-2507', + input_cost_per_token: 8e-8, + output_cost_per_token: 3.3e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-235b-a22b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-30b-a3b-thinking-2507', + input_cost_per_token: 8e-8, + output_cost_per_token: 2.9e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-30b-a3b', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, + model_id: 'qwen/qwen3-32b', + input_cost_per_token: 5e-8, + output_cost_per_token: 2e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-30b-a3b-instruct-2507', - input_cost_per_token: 1e-7, - output_cost_per_token: 3e-7, + model_id: 'qwen/qwen3-8b', + input_cost_per_token: 3.5e-8, + output_cost_per_token: 1.38e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-30b-a3b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-coder', + input_cost_per_token: 2.2e-7, + output_cost_per_token: 9.5e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-32b', - input_cost_per_token: 1.7992692e-8, - output_cost_per_token: 7.200576e-8, + model_id: 'qwen/qwen3-coder-30b-a3b-instruct', + input_cost_per_token: 6e-8, + output_cost_per_token: 2.5e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-4b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-coder-flash', + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000015, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-8b', - input_cost_per_token: 3.5e-8, - output_cost_per_token: 1.38e-7, + model_id: 'qwen/qwen3-coder-plus', + input_cost_per_token: 0.000001, + output_cost_per_token: 0.000005, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-8b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-max', + input_cost_per_token: 0.0000012, + output_cost_per_token: 0.000006, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-coder', - input_cost_per_token: 2e-7, + model_id: 'qwen/qwen3-next-80b-a3b-instruct', + input_cost_per_token: 1e-7, output_cost_per_token: 8e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwen3-coder:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-vl-235b-a22b-instruct', + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { - model_id: 'qwen/qwq-32b', - input_cost_per_token: 7.5e-8, - output_cost_per_token: 1.5e-7, + model_id: 'qwen/qwen3-vl-235b-a22b-thinking', + input_cost_per_token: 4.5e-7, + output_cost_per_token: 0.0000035, provider: 'OpenRouter', }, { - model_id: 'qwen/qwq-32b-preview', - input_cost_per_token: 2e-7, - output_cost_per_token: 2e-7, + model_id: 'qwen/qwen3-vl-30b-a3b-instruct', + input_cost_per_token: 2.9e-7, + output_cost_per_token: 9.9e-7, provider: 'OpenRouter', }, { - model_id: 'qwen/qwq-32b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwen3-vl-30b-a3b-thinking', + input_cost_per_token: 2.9e-7, + output_cost_per_token: 0.000001, provider: 'OpenRouter', }, { - model_id: 'raifle/sorcererlm-8x22b', - input_cost_per_token: 0.0000045, - output_cost_per_token: 0.0000045, + model_id: 'qwen/qwen3-vl-8b-instruct', + input_cost_per_token: 1.8e-7, + output_cost_per_token: 6.9e-7, + provider: 'OpenRouter', + }, + { + model_id: 'qwen/qwen3-vl-8b-thinking', + input_cost_per_token: 1.8e-7, + output_cost_per_token: 0.0000021, + provider: 'OpenRouter', + }, + { + model_id: 'qwen/qwq-32b', + input_cost_per_token: 1.5e-7, + output_cost_per_token: 4e-7, provider: 'OpenRouter', }, { - model_id: 'rekaai/reka-flash-3:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'raifle/sorcererlm-8x22b', + input_cost_per_token: 0.0000045, + output_cost_per_token: 0.0000045, provider: 'OpenRouter', }, { @@ -2024,10 +1709,16 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'sao10k/l3-lunaris-8b', - input_cost_per_token: 2e-8, + input_cost_per_token: 4e-8, output_cost_per_token: 5e-8, provider: 'OpenRouter', }, + { + model_id: 'sao10k/l3.1-70b-hanami-x1', + input_cost_per_token: 0.000003, + output_cost_per_token: 0.000003, + provider: 'OpenRouter', + }, { model_id: 'sao10k/l3.1-euryale-70b', input_cost_per_token: 6.5e-7, @@ -2040,34 +1731,16 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 7.5e-7, provider: 'OpenRouter', }, - { - model_id: 'sarvamai/sarvam-m:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'scb10x/llama3.1-typhoon2-70b-instruct', - input_cost_per_token: 8.8e-7, - output_cost_per_token: 8.8e-7, - provider: 'OpenRouter', - }, { model_id: 'shisa-ai/shisa-v2-llama3.3-70b', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, - provider: 'OpenRouter', - }, - { - model_id: 'shisa-ai/shisa-v2-llama3.3-70b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 5e-8, + output_cost_per_token: 2.2e-7, provider: 'OpenRouter', }, { - model_id: 'sophosympatheia/midnight-rose-70b', - input_cost_per_token: 8e-7, - output_cost_per_token: 8e-7, + model_id: 'stepfun-ai/step3', + input_cost_per_token: 5.7e-7, + output_cost_per_token: 0.00000142, provider: 'OpenRouter', }, { @@ -2082,22 +1755,16 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 3e-8, provider: 'OpenRouter', }, - { - model_id: 'tencent/hunyuan-a13b-instruct:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, { model_id: 'thedrummer/anubis-70b-v1.1', - input_cost_per_token: 4e-7, - output_cost_per_token: 7e-7, + input_cost_per_token: 6.5e-7, + output_cost_per_token: 0.000001, provider: 'OpenRouter', }, { - model_id: 'thedrummer/anubis-pro-105b-v1', - input_cost_per_token: 5e-7, - output_cost_per_token: 0.000001, + model_id: 'thedrummer/cydonia-24b-v4.1', + input_cost_per_token: 3e-7, + output_cost_per_token: 5e-7, provider: 'OpenRouter', }, { @@ -2108,8 +1775,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'thedrummer/skyfall-36b-v2', - input_cost_per_token: 4.81286e-8, - output_cost_per_token: 1.92608e-7, + input_cost_per_token: 8e-8, + output_cost_per_token: 3.3e-7, provider: 'OpenRouter', }, { @@ -2118,12 +1785,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 4e-7, provider: 'OpenRouter', }, - { - model_id: 'thudm/glm-4-32b', - input_cost_per_token: 5.5e-7, - output_cost_per_token: 0.00000166, - provider: 'OpenRouter', - }, { model_id: 'thudm/glm-4.1v-9b-thinking', input_cost_per_token: 3.5e-8, @@ -2132,26 +1793,20 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'thudm/glm-z1-32b', - input_cost_per_token: 1.999188e-8, - output_cost_per_token: 8.00064e-8, + input_cost_per_token: 5e-8, + output_cost_per_token: 2.2e-7, provider: 'OpenRouter', }, { model_id: 'tngtech/deepseek-r1t-chimera', - input_cost_per_token: 1.999188e-7, - output_cost_per_token: 8.00064e-7, - provider: 'OpenRouter', - }, - { - model_id: 'tngtech/deepseek-r1t-chimera:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { - model_id: 'tngtech/deepseek-r1t2-chimera:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'tngtech/deepseek-r1t2-chimera', + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { @@ -2160,18 +1815,6 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 6.5e-7, provider: 'OpenRouter', }, - { - model_id: 'x-ai/grok-2-1212', - input_cost_per_token: 0.000002, - output_cost_per_token: 0.00001, - provider: 'OpenRouter', - }, - { - model_id: 'x-ai/grok-2-vision-1212', - input_cost_per_token: 0.000002, - output_cost_per_token: 0.00001, - provider: 'OpenRouter', - }, { model_id: 'x-ai/grok-3', input_cost_per_token: 0.000003, @@ -2203,15 +1846,15 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'x-ai/grok-code-fast-1', - input_cost_per_token: 0.0000002, - output_cost_per_token: 0.0000015, + model_id: 'x-ai/grok-4-fast', + input_cost_per_token: 2e-7, + output_cost_per_token: 5e-7, provider: 'OpenRouter', }, { - model_id: 'x-ai/grok-vision-beta', - input_cost_per_token: 0.000005, - output_cost_per_token: 0.000015, + model_id: 'x-ai/grok-code-fast-1', + input_cost_per_token: 2e-7, + output_cost_per_token: 0.0000015, provider: 'OpenRouter', }, { @@ -2222,26 +1865,26 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'z-ai/glm-4.5', - input_cost_per_token: 1.999188e-7, - output_cost_per_token: 8.00064e-7, + input_cost_per_token: 3.5e-7, + output_cost_per_token: 0.00000155, provider: 'OpenRouter', }, { model_id: 'z-ai/glm-4.5-air', - input_cost_per_token: 2e-7, - output_cost_per_token: 0.0000011, + input_cost_per_token: 1.4e-7, + output_cost_per_token: 8.6e-7, provider: 'OpenRouter', }, { - model_id: 'z-ai/glm-4.5-air:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'z-ai/glm-4.5v', + input_cost_per_token: 6e-7, + output_cost_per_token: 0.0000018, provider: 'OpenRouter', }, { - model_id: 'z-ai/glm-4.5v', + model_id: 'z-ai/glm-4.6', input_cost_per_token: 5e-7, - output_cost_per_token: 0.0000018, + output_cost_per_token: 0.00000175, provider: 'OpenRouter', }, ]; diff --git a/packages/tests/provider-smoke/gemini-generate-text.test.ts b/packages/tests/provider-smoke/gemini-generate-text.test.ts index d25ca33b6..da2339df1 100644 --- a/packages/tests/provider-smoke/gemini-generate-text.test.ts +++ b/packages/tests/provider-smoke/gemini-generate-text.test.ts @@ -14,6 +14,15 @@ import { beforeAll(assertEnv); +export const BLACKLISTED_MODELS = new Set([ + 'gemini-2.0-flash-preview-image-generation', + 'veo-3.0-fast-generate', + 'gemini-2.0-flash-exp', + 'gemini-2.0-flash-thinking-exp-1219', + 'gemini-2.5-pro-preview-tts', + 'gemini-2.5-flash-preview-tts', +]); + describe.concurrent('Gemini generateText per model', () => { const gemini = createEchoGoogle( { appId: ECHO_APP_ID!, baseRouterUrl }, @@ -21,6 +30,10 @@ describe.concurrent('Gemini generateText per model', () => { ); for (const { model_id } of GeminiModels) { + if (BLACKLISTED_MODELS.has(model_id)) { + console.log('Skipping generateText for blacklisted model', model_id); + continue; + } it(`Gemini ${model_id}`, async () => { try { const { text } = await generateText({ diff --git a/packages/tests/provider-smoke/gemini-stream-text.test.ts b/packages/tests/provider-smoke/gemini-stream-text.test.ts index 64b7a1892..086995cea 100644 --- a/packages/tests/provider-smoke/gemini-stream-text.test.ts +++ b/packages/tests/provider-smoke/gemini-stream-text.test.ts @@ -3,11 +3,12 @@ import { GeminiModels, } from '@merit-systems/echo-typescript-sdk'; import { streamText } from 'ai'; +import { BLACKLISTED_MODELS } from 'gemini-generate-text.test'; import { beforeAll, describe, expect, it } from 'vitest'; import { - ECHO_APP_ID, assertEnv, baseRouterUrl, + ECHO_APP_ID, getApiErrorDetails, getToken, } from './test-helpers'; @@ -21,6 +22,10 @@ describe.concurrent('Gemini streamText per model', () => { ); for (const { model_id } of GeminiModels) { + if (BLACKLISTED_MODELS.has(model_id)) { + console.log('Skipping generateText for blacklisted model', model_id); + continue; + } it(`Gemini streamText ${model_id}`, async () => { try { const { textStream } = streamText({ diff --git a/packages/tests/provider-smoke/openai-generate-text.test.ts b/packages/tests/provider-smoke/openai-generate-text.test.ts index 784e5d4c1..0af11cddc 100644 --- a/packages/tests/provider-smoke/openai-generate-text.test.ts +++ b/packages/tests/provider-smoke/openai-generate-text.test.ts @@ -15,6 +15,29 @@ import { beforeAll(assertEnv); +export const BLACKLISTED_MODELS = new Set([ + 'gpt-4o-search-preview-2025-03-11', + 'gpt-3.5-turbo-instruct', + 'gpt-3.5-turbo-instruct-0914', + 'gpt-5-pro', + 'gpt-5-pro-2025-10-06', + 'o1-pro', + 'o3-deep-research', + 'o3-deep-research-2025-06-26', + 'o3-pro', + 'o3-pro-2025-06-10', + 'o1-mini-2024-09-12', + 'gpt-3.5-turbo-16k', + 'gpt-4o-mini-search-preview', + 'gpt-4o-mini-search-preview-2025-03-11', + 'gpt-4o-search-preview', + 'gpt-5-search-api', + 'gpt-5-search-api-2025-10-14', + 'o1-mini', + 'o1-pro-2025-03-19', + 'o3-2025-04-16', +]); + describe.concurrent('OpenAI generateText per model', () => { const openai = createEchoOpenAI( { appId: ECHO_APP_ID!, baseRouterUrl }, @@ -22,6 +45,10 @@ describe.concurrent('OpenAI generateText per model', () => { ); for (const { model_id } of OpenAIModels) { + if (BLACKLISTED_MODELS.has(model_id)) { + console.log('Skipping generateText for blacklisted model', model_id); + continue; + } it(`OpenAI ${model_id}`, async () => { try { const tools = getOpenAITools(openai, model_id); @@ -36,6 +63,6 @@ describe.concurrent('OpenAI generateText per model', () => { const details = getApiErrorDetails(err); throw new Error(`[generateText] OpenAI ${model_id} failed: ${details}`); } - }); + }, 15_000); } }); diff --git a/packages/tests/provider-smoke/openai-stream-text.test.ts b/packages/tests/provider-smoke/openai-stream-text.test.ts index 6d86f29cb..038fd60b1 100644 --- a/packages/tests/provider-smoke/openai-stream-text.test.ts +++ b/packages/tests/provider-smoke/openai-stream-text.test.ts @@ -3,6 +3,7 @@ import { createEchoOpenAI, } from '@merit-systems/echo-typescript-sdk'; import { ToolSet, streamText } from 'ai'; +import { BLACKLISTED_MODELS } from 'openai-generate-text.test'; import { beforeAll, describe, expect, it } from 'vitest'; import { ECHO_APP_ID, @@ -22,6 +23,11 @@ describe.concurrent('OpenAI streamText per model', () => { ); for (const { model_id } of OpenAIModels) { + if (BLACKLISTED_MODELS.has(model_id)) { + console.log('Skipping streamText for blacklisted model', model_id); + continue; + } + it(`OpenAI stream ${model_id}`, async () => { try { const tools = getOpenAITools(openai, model_id); diff --git a/packages/tests/provider-smoke/openrouter-generate-text.test.ts b/packages/tests/provider-smoke/openrouter-generate-text.test.ts index 7d68f00dc..026cc979e 100644 --- a/packages/tests/provider-smoke/openrouter-generate-text.test.ts +++ b/packages/tests/provider-smoke/openrouter-generate-text.test.ts @@ -10,6 +10,7 @@ import { baseRouterUrl, getApiErrorDetails, getToken, + shouldSkipModelInTests, } from './test-helpers'; beforeAll(assertEnv); @@ -21,6 +22,10 @@ describe.concurrent('OpenRouter generateText per model', () => { ); for (const { model_id } of OpenRouterModels) { + if (shouldSkipModelInTests(model_id)) { + continue; + } + it(`OpenAI ${model_id}`, async () => { try { const { text } = await generateText({ @@ -35,6 +40,6 @@ describe.concurrent('OpenRouter generateText per model', () => { `[generateText] OpenRouter ${model_id} failed: ${details}` ); } - }); + }, 45_000); // 15 second timeout per test } }); diff --git a/packages/tests/provider-smoke/openrouter-stream-text.test.ts b/packages/tests/provider-smoke/openrouter-stream-text.test.ts index 2c28ffbc0..7253e6694 100644 --- a/packages/tests/provider-smoke/openrouter-stream-text.test.ts +++ b/packages/tests/provider-smoke/openrouter-stream-text.test.ts @@ -10,10 +10,18 @@ import { baseRouterUrl, getApiErrorDetails, getToken, + shouldSkipModelInTests, } from './test-helpers'; beforeAll(assertEnv); +const BLACKLISTED_MODELS = new Set([ + 'deepseek/deepseek-r1-distill-qwen-14b', + 'qwen/qwen3-30b-a3b', + 'thudm/glm-z1-32b', + 'qwen/qwen3-coder', +]); + describe.concurrent('OpenAI streamText per model', () => { const openrouter = createEchoOpenRouter( { appId: ECHO_APP_ID!, baseRouterUrl }, @@ -21,6 +29,13 @@ describe.concurrent('OpenAI streamText per model', () => { ); for (const { model_id } of OpenRouterModels) { + if (shouldSkipModelInTests(model_id)) { + continue; + } + if (BLACKLISTED_MODELS.has(model_id)) { + console.log('Skipping streamText for blacklisted model', model_id); + continue; + } it(`OpenRouter stream ${model_id}`, async () => { try { const { textStream } = streamText({ @@ -38,6 +53,6 @@ describe.concurrent('OpenAI streamText per model', () => { `[streamText] OpenRouter ${model_id} failed: ${details}` ); } - }); + }, 45000); // 45 second timeout per test } }); diff --git a/packages/tests/provider-smoke/test-helpers.ts b/packages/tests/provider-smoke/test-helpers.ts index a7e43bbf6..f47b13b0e 100644 --- a/packages/tests/provider-smoke/test-helpers.ts +++ b/packages/tests/provider-smoke/test-helpers.ts @@ -1,5 +1,3 @@ -import type { EchoOpenAIProvider } from '@merit-systems/echo-typescript-sdk'; - export const ECHO_TOKEN = process.env.ECHO_API_KEY; export const ECHO_APP_ID = process.env.ECHO_APP_ID; export const baseRouterUrl = @@ -35,3 +33,64 @@ export function getOpenAITools( } : undefined; } + +// OpenRouter models that are too slow for smoke tests (>15s) +const BLACKLISTED_OR_SLOW_MODELS = new Set([ + 'mistralai/magistral-medium-2506:thinking', + 'z-ai/glm-4.6', + 'openai/gpt-5-pro', + 'openai/o4-mini-deep-research', + 'meta-llama/llama-guard-2-8b', + 'eleutherai/llemma_7b', + 'alibaba/tongyi-deepresearch-30b-a3b', + 'stepfun-ai/step3', + 'minimax/minimax-m1', + 'sao10k/l3.1-70b-hanami-x1', + 'qwen/qwen-2.5-vl-7b-instruct', + 'deepseek/deepseek-r1-distill-qwen-32b', + 'cohere/command-r-plus-08-2024', + 'google/gemini-2.5-pro', + 'google/gemini-2.5-pro-preview-05-06', + 'meta-llama/llama-3.1-405b', + 'google/gemini-2.5-pro-preview', + 'meta-llama/llama-guard-3-8b', + 'moonshotai/kimi-dev-72b', + 'openai/o1-pro', + 'openai/o3-pro', + 'z-ai/glm-4.5', + 'morph/morph-v3-fast', + 'tngtech/deepseek-r1t-chimera', + 'perplexity/sonar-reasoning-pro', + 'qwen/qwq-32b', + 'nousresearch/hermes-3-llama-3.1-405b', + 'openai/o3', + 'perplexity/sonar', + 'perplexity/sonar-reasoning', + 'qwen/qwen3-32b', + 'deepseek/deepseek-r1-0528', + 'deepseek/deepseek-r1-0528-qwen3-8b', + 'openai/o1', + 'qwen/qwen3-235b-a22b', + 'qwen/qwen3-8b', + 'z-ai/glm-4.5-air', + 'aion-labs/aion-1.0-mini', + 'perplexity/sonar-pro', + 'openai/gpt-oss-120b', + 'deepseek/deepseek-chat-v3.1', + 'deepseek/deepseek-v3.2-exp', + 'google/gemma-2-9b-it', + 'qwen/qwen3-coder', + 'google/gemma-2-27b-it', + 'thudm/glm-z1-32b', + 'deepseek/deepseek-v3.1-terminus', +]); + +export function shouldSkipModelInTests(model_id: string): boolean { + if (BLACKLISTED_OR_SLOW_MODELS.has(model_id)) { + return true; + } + if (model_id.includes('thinking') || model_id.includes('deep-research')) { + return true; + } + return false; +} From d7b37287c6f5082c89a298769847dccda0111c3b Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 12:02:01 -0400 Subject: [PATCH 33/50] path --- packages/app/server/src/services/EchoControlService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index 8cbe67626..dca4e6558 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` + From 208135b35c929723402bf6a1295302e9f25c8836 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 12:08:41 -0400 Subject: [PATCH 34/50] optimize this docker a bit --- Dockerfile.railway | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Dockerfile.railway b/Dockerfile.railway index ee055678e..320efb7c9 100644 --- a/Dockerfile.railway +++ b/Dockerfile.railway @@ -69,41 +69,48 @@ RUN apk add --no-cache \ git \ openssh-client -# Copy pnpm workspace and lock files for dependency installation +# Copy dependency files for caching COPY pnpm-lock.yaml ./ COPY pnpm-workspace.yaml ./ COPY package.json ./ -COPY tsconfig.base.json ./ COPY turbo.json ./ -# Copy all project files first +# Copy only package.json files first for better caching +COPY packages/app/control/package.json ./packages/app/control/package.json +COPY packages/app/server/package.json ./packages/app/server/package.json +COPY packages/sdk/ts/package.json ./packages/sdk/ts/package.json +COPY packages/sdk/next/package.json ./packages/sdk/next/package.json +COPY packages/sdk/react/package.json ./packages/sdk/react/package.json +COPY packages/sdk/examples/next/package.json ./packages/sdk/examples/next/package.json +COPY packages/sdk/examples/vite/package.json ./packages/sdk/examples/vite/package.json +COPY packages/sdk/component-registry/package.json ./packages/sdk/component-registry/package.json + +# Install dependencies first (cached if package.json unchanged) +WORKDIR /app +RUN pnpm install --frozen-lockfile + +# Copy source code after deps are installed COPY packages/app/control/ ./packages/app/control/ COPY packages/app/server/ ./packages/app/server/ COPY packages/sdk/ts/ ./packages/sdk/ts/ COPY packages/sdk/next/ ./packages/sdk/next/ COPY packages/sdk/react/ ./packages/sdk/react/ -# we are now also copy the examples because pnpm install needs it COPY packages/sdk/examples/next/ ./packages/sdk/examples/next/ COPY packages/sdk/examples/vite/ ./packages/sdk/examples/vite/ COPY packages/sdk/component-registry/ ./packages/sdk/component-registry/ +COPY tsconfig.base.json ./ -# Install all workspace dependencies -WORKDIR /app -RUN pnpm install - -# Build only what's needed for the server -WORKDIR /app -RUN SKIP_ENV_VALIDATION=true pnpm exec turbo run build --filter=echo-server - -# Generate Prisma client and copy schema for server +# Generate Prisma client first (before build) WORKDIR /app/packages/app/control RUN pnpm run prisma:generate +# Copy Prisma files to server WORKDIR /app/packages/app/server -RUN pnpm run copy-prisma +RUN pnpm run copy-schema && pnpm run prisma:generate -# Step 4: Install production dependencies only -RUN pnpm install +# Build only the server package +WORKDIR /app +RUN SKIP_ENV_VALIDATION=true pnpm exec turbo run build --filter=echo-server # Expose the port that echo-server runs on EXPOSE 3069 From f27762a4389418138a82bf570d88102140d13240 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 12:37:27 -0400 Subject: [PATCH 35/50] docker 1 --- Dockerfile.railway | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/Dockerfile.railway b/Dockerfile.railway index 320efb7c9..440ccf948 100644 --- a/Dockerfile.railway +++ b/Dockerfile.railway @@ -75,21 +75,7 @@ COPY pnpm-workspace.yaml ./ COPY package.json ./ COPY turbo.json ./ -# Copy only package.json files first for better caching -COPY packages/app/control/package.json ./packages/app/control/package.json -COPY packages/app/server/package.json ./packages/app/server/package.json -COPY packages/sdk/ts/package.json ./packages/sdk/ts/package.json -COPY packages/sdk/next/package.json ./packages/sdk/next/package.json -COPY packages/sdk/react/package.json ./packages/sdk/react/package.json -COPY packages/sdk/examples/next/package.json ./packages/sdk/examples/next/package.json -COPY packages/sdk/examples/vite/package.json ./packages/sdk/examples/vite/package.json -COPY packages/sdk/component-registry/package.json ./packages/sdk/component-registry/package.json - -# Install dependencies first (cached if package.json unchanged) -WORKDIR /app -RUN pnpm install --frozen-lockfile - -# Copy source code after deps are installed +# Copy all source code (needed for postinstall scripts) COPY packages/app/control/ ./packages/app/control/ COPY packages/app/server/ ./packages/app/server/ COPY packages/sdk/ts/ ./packages/sdk/ts/ @@ -100,6 +86,10 @@ COPY packages/sdk/examples/vite/ ./packages/sdk/examples/vite/ COPY packages/sdk/component-registry/ ./packages/sdk/component-registry/ COPY tsconfig.base.json ./ +# Install dependencies (needs source files for postinstall scripts) +WORKDIR /app +RUN pnpm install --frozen-lockfile + # Generate Prisma client first (before build) WORKDIR /app/packages/app/control RUN pnpm run prisma:generate From 793861c2ab53bd65310ac50aace532ff080d7c9b Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 12:53:54 -0400 Subject: [PATCH 36/50] revert docker --- Dockerfile.railway | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Dockerfile.railway b/Dockerfile.railway index 440ccf948..ab6732f33 100644 --- a/Dockerfile.railway +++ b/Dockerfile.railway @@ -69,38 +69,41 @@ RUN apk add --no-cache \ git \ openssh-client -# Copy dependency files for caching +# Copy pnpm workspace and lock files for dependency installation COPY pnpm-lock.yaml ./ COPY pnpm-workspace.yaml ./ COPY package.json ./ +COPY tsconfig.base.json ./ COPY turbo.json ./ -# Copy all source code (needed for postinstall scripts) +# Copy all project files first COPY packages/app/control/ ./packages/app/control/ COPY packages/app/server/ ./packages/app/server/ COPY packages/sdk/ts/ ./packages/sdk/ts/ COPY packages/sdk/next/ ./packages/sdk/next/ COPY packages/sdk/react/ ./packages/sdk/react/ +# we are now also copy the examples because pnpm install needs it COPY packages/sdk/examples/next/ ./packages/sdk/examples/next/ COPY packages/sdk/examples/vite/ ./packages/sdk/examples/vite/ COPY packages/sdk/component-registry/ ./packages/sdk/component-registry/ -COPY tsconfig.base.json ./ -# Install dependencies (needs source files for postinstall scripts) +# Install all workspace dependencies WORKDIR /app -RUN pnpm install --frozen-lockfile +RUN pnpm install -# Generate Prisma client first (before build) +# Build only what's needed for the server +WORKDIR /app +RUN SKIP_ENV_VALIDATION=true pnpm exec turbo run build --filter=echo-server + +# Generate Prisma client and copy schema for server WORKDIR /app/packages/app/control RUN pnpm run prisma:generate -# Copy Prisma files to server WORKDIR /app/packages/app/server -RUN pnpm run copy-schema && pnpm run prisma:generate +RUN pnpm run copy-prisma -# Build only the server package -WORKDIR /app -RUN SKIP_ENV_VALIDATION=true pnpm exec turbo run build --filter=echo-server +# Step 4: Install production dependencies only +RUN pnpm install --prod # Expose the port that echo-server runs on EXPOSE 3069 From 08c29d9bfba427e5a168750f73fc6340cb368515 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 16:00:42 -0400 Subject: [PATCH 37/50] big boxes for gha --- .github/workflows/integration-tests.yml | 4 ++-- .github/workflows/provider-smoke-tests.yml | 2 +- .github/workflows/publish-sdks.yml | 4 ++-- .github/workflows/test-publish.yml | 2 +- Dockerfile.railway | 4 ++++ 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index c13db1625..06bb0d3a7 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -25,7 +25,7 @@ on: jobs: integration-tests: name: Run Integration Tests - runs-on: ubuntu-latest + runs-on: ubuntu-latest-l timeout-minutes: 30 env: @@ -309,7 +309,7 @@ jobs: e2e-tests: name: End-to-End Tests - runs-on: ubuntu-latest + runs-on: ubuntu-latest-l timeout-minutes: 20 needs: integration-tests if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-e2e') diff --git a/.github/workflows/provider-smoke-tests.yml b/.github/workflows/provider-smoke-tests.yml index 2869ff26c..536c78935 100644 --- a/.github/workflows/provider-smoke-tests.yml +++ b/.github/workflows/provider-smoke-tests.yml @@ -13,7 +13,7 @@ on: jobs: provider-smoke-tests: name: Run Provider Smoke Tests - runs-on: ubuntu-latest + runs-on: ubuntu-latest-l timeout-minutes: 40 steps: diff --git a/.github/workflows/publish-sdks.yml b/.github/workflows/publish-sdks.yml index 85eb584a3..7f7b9b08c 100644 --- a/.github/workflows/publish-sdks.yml +++ b/.github/workflows/publish-sdks.yml @@ -14,7 +14,7 @@ on: jobs: publish: name: Build and publish SDK packages - runs-on: ubuntu-latest + runs-on: ubuntu-latest-l if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/production' || github.event_name == 'workflow_dispatch' outputs: typescript-published: ${{ steps.publish-typescript.outputs.published }} @@ -275,7 +275,7 @@ jobs: create-tag: name: Create version tag - runs-on: ubuntu-latest + runs-on: ubuntu-latest-l needs: publish if: (needs.publish.outputs.typescript-published == 'true' || needs.publish.outputs.react-published == 'true' || needs.publish.outputs.next-published == 'true' || needs.publish.outputs.aix402-published == 'true' || needs.publish.outputs.echo-start-published == 'true') && github.ref == 'refs/heads/production' && !startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml index da239d8a7..55df13e66 100644 --- a/.github/workflows/test-publish.yml +++ b/.github/workflows/test-publish.yml @@ -26,7 +26,7 @@ on: jobs: test-publish: name: Test SDK publishing workflow - runs-on: ubuntu-latest + runs-on: ubuntu-latest-l outputs: typescript-ready: ${{ steps.test-typescript.outputs.ready }} react-ready: ${{ steps.test-react.outputs.ready }} diff --git a/Dockerfile.railway b/Dockerfile.railway index ab6732f33..d4bd81bbc 100644 --- a/Dockerfile.railway +++ b/Dockerfile.railway @@ -91,6 +91,10 @@ COPY packages/sdk/component-registry/ ./packages/sdk/component-registry/ WORKDIR /app RUN pnpm install +# Build SDK first to ensure latest version is used +WORKDIR /app +RUN pnpm exec turbo run build --filter=@merit-systems/echo-typescript-sdk + # Build only what's needed for the server WORKDIR /app RUN SKIP_ENV_VALIDATION=true pnpm exec turbo run build --filter=echo-server From 1b6daa19ca7a78e052ace498595b8aaed8c87a9e Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 17:11:37 -0400 Subject: [PATCH 38/50] revert docker --- .github/workflows/integration-tests.yml | 4 ++-- .github/workflows/provider-smoke-tests.yml | 2 +- .github/workflows/publish-sdks.yml | 4 ++-- .github/workflows/test-publish.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 06bb0d3a7..c13db1625 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -25,7 +25,7 @@ on: jobs: integration-tests: name: Run Integration Tests - runs-on: ubuntu-latest-l + runs-on: ubuntu-latest timeout-minutes: 30 env: @@ -309,7 +309,7 @@ jobs: e2e-tests: name: End-to-End Tests - runs-on: ubuntu-latest-l + runs-on: ubuntu-latest timeout-minutes: 20 needs: integration-tests if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'run-e2e') diff --git a/.github/workflows/provider-smoke-tests.yml b/.github/workflows/provider-smoke-tests.yml index 536c78935..2869ff26c 100644 --- a/.github/workflows/provider-smoke-tests.yml +++ b/.github/workflows/provider-smoke-tests.yml @@ -13,7 +13,7 @@ on: jobs: provider-smoke-tests: name: Run Provider Smoke Tests - runs-on: ubuntu-latest-l + runs-on: ubuntu-latest timeout-minutes: 40 steps: diff --git a/.github/workflows/publish-sdks.yml b/.github/workflows/publish-sdks.yml index 7f7b9b08c..85eb584a3 100644 --- a/.github/workflows/publish-sdks.yml +++ b/.github/workflows/publish-sdks.yml @@ -14,7 +14,7 @@ on: jobs: publish: name: Build and publish SDK packages - runs-on: ubuntu-latest-l + runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/production' || github.event_name == 'workflow_dispatch' outputs: typescript-published: ${{ steps.publish-typescript.outputs.published }} @@ -275,7 +275,7 @@ jobs: create-tag: name: Create version tag - runs-on: ubuntu-latest-l + runs-on: ubuntu-latest needs: publish if: (needs.publish.outputs.typescript-published == 'true' || needs.publish.outputs.react-published == 'true' || needs.publish.outputs.next-published == 'true' || needs.publish.outputs.aix402-published == 'true' || needs.publish.outputs.echo-start-published == 'true') && github.ref == 'refs/heads/production' && !startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml index 55df13e66..da239d8a7 100644 --- a/.github/workflows/test-publish.yml +++ b/.github/workflows/test-publish.yml @@ -26,7 +26,7 @@ on: jobs: test-publish: name: Test SDK publishing workflow - runs-on: ubuntu-latest-l + runs-on: ubuntu-latest outputs: typescript-ready: ${{ steps.test-typescript.outputs.ready }} react-ready: ${{ steps.test-react.outputs.ready }} From cfd90d69c8f35fe4848ddf05d5de49d8edd12642 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Fri, 17 Oct 2025 03:08:14 +0530 Subject: [PATCH 39/50] Refactor GroqProvider and update Groq model costs --- .../app/server/src/providers/GroqProvider.ts | 47 +---------- .../sdk/ts/src/supported-models/chat/groq.ts | 78 +++++++++++++++---- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/packages/app/server/src/providers/GroqProvider.ts b/packages/app/server/src/providers/GroqProvider.ts index 31c75371c..b771055c5 100644 --- a/packages/app/server/src/providers/GroqProvider.ts +++ b/packages/app/server/src/providers/GroqProvider.ts @@ -2,54 +2,9 @@ import { LlmTransactionMetadata, Transaction } from '../types'; import { getCostPerToken } from '../services/AccountingService'; import { BaseProvider } from './BaseProvider'; import { ProviderType } from './ProviderType'; +import { CompletionStateBody, parseSSEGPTFormat } from './GPTProvider'; import logger from '../logger'; -export interface CompletionStateBody { - id: string; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -export interface StreamingChunkBody { - id: string; - choices: { - index: number; - delta: { - content?: string; - }; - finish_reason: string | null; - }[]; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - } | null; -} - -export const parseSSEGPTFormat = (data: string): StreamingChunkBody[] => { - const events = data.split('\n\n'); - const chunks: StreamingChunkBody[] = []; - - for (const event of events) { - if (!event.trim()) continue; - if (event.startsWith('data: ')) { - const jsonStr = event.slice(6); - if (jsonStr.trim() === '[DONE]') continue; - try { - const parsed = JSON.parse(jsonStr); - chunks.push(parsed); - } catch (error) { - logger.error(`Error parsing SSE chunk: ${error}`); - } - } - } - - return chunks; -}; - export class GroqProvider extends BaseProvider { private readonly GROQ_BASE_URL = 'https://api.groq.com/openai/v1'; diff --git a/packages/sdk/ts/src/supported-models/chat/groq.ts b/packages/sdk/ts/src/supported-models/chat/groq.ts index c94697a2d..73474ba4f 100644 --- a/packages/sdk/ts/src/supported-models/chat/groq.ts +++ b/packages/sdk/ts/src/supported-models/chat/groq.ts @@ -1,48 +1,98 @@ import { SupportedModel } from '../types'; +// Groq model IDs export type GroqModel = | 'llama3-8b-8192' | 'llama3-70b-8192' | 'mixtral-8x7b-32768' | 'gemma2-9b-it' | 'llama-3.1-8b-instant' - | 'llama-3.1-70b-versatile'; + | 'llama-3.3-70b-versatile' + | 'llama-4-scout' + | 'llama-4-maverick' + | 'llama-guard-4-12b' + | 'qwen3-32b' + | 'gpt-oss-20b' + | 'gpt-oss-120b' + | 'kimi-k2-0905-1t'; export const GroqModels: SupportedModel[] = [ { model_id: 'llama3-8b-8192', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 0.00000005, + output_cost_per_token: 0.00000008, provider: 'Groq', }, { model_id: 'llama3-70b-8192', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 0.00000027, + output_cost_per_token: 0.00000027, provider: 'Groq', }, { model_id: 'mixtral-8x7b-32768', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 0.00000027, + output_cost_per_token: 0.00000027, provider: 'Groq', }, { model_id: 'gemma2-9b-it', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 0.00000007, + output_cost_per_token: 0.00000007, provider: 'Groq', }, { model_id: 'llama-3.1-8b-instant', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 0.00000005, + output_cost_per_token: 0.00000008, provider: 'Groq', }, { - model_id: 'llama-3.1-70b-versatile', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'llama-3.3-70b-versatile', + input_cost_per_token: 0.00000059, + output_cost_per_token: 0.00000079, + provider: 'Groq', + }, + { + model_id: 'llama-4-scout', + input_cost_per_token: 0.00000011, + output_cost_per_token: 0.00000034, + provider: 'Groq', + }, + { + model_id: 'llama-4-maverick', + input_cost_per_token: 0.0000002, + output_cost_per_token: 0.0000006, + provider: 'Groq', + }, + { + model_id: 'llama-guard-4-12b', + input_cost_per_token: 0.0000002, + output_cost_per_token: 0.0000002, + provider: 'Groq', + }, + { + model_id: 'qwen3-32b', + input_cost_per_token: 0.00000029, + output_cost_per_token: 0.00000059, + provider: 'Groq', + }, + { + model_id: 'gpt-oss-20b', + input_cost_per_token: 0.000000075, + output_cost_per_token: 0.0000003, + provider: 'Groq', + }, + { + model_id: 'gpt-oss-120b', + input_cost_per_token: 0.00000015, + output_cost_per_token: 0.0000006, + provider: 'Groq', + }, + { + model_id: 'kimi-k2-0905-1t', + input_cost_per_token: 0.000001, + output_cost_per_token: 0.000003, provider: 'Groq', }, ]; From f6dc1b57971563b8c0d71c8cbaa4ced47ccf1c05 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 17:47:04 -0400 Subject: [PATCH 40/50] update test name or --- packages/tests/provider-smoke/openrouter-generate-text.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/provider-smoke/openrouter-generate-text.test.ts b/packages/tests/provider-smoke/openrouter-generate-text.test.ts index 026cc979e..43c319e06 100644 --- a/packages/tests/provider-smoke/openrouter-generate-text.test.ts +++ b/packages/tests/provider-smoke/openrouter-generate-text.test.ts @@ -26,7 +26,7 @@ describe.concurrent('OpenRouter generateText per model', () => { continue; } - it(`OpenAI ${model_id}`, async () => { + it(`OpenRouter generateText ${model_id}`, async () => { try { const { text } = await generateText({ model: openrouter(model_id), From ee90834e1e4497f93e74a2b0b8e22886adbfff10 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 17:47:37 -0400 Subject: [PATCH 41/50] rm timeouts --- packages/tests/provider-smoke/openrouter-generate-text.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/provider-smoke/openrouter-generate-text.test.ts b/packages/tests/provider-smoke/openrouter-generate-text.test.ts index 43c319e06..b12ef0d04 100644 --- a/packages/tests/provider-smoke/openrouter-generate-text.test.ts +++ b/packages/tests/provider-smoke/openrouter-generate-text.test.ts @@ -40,6 +40,6 @@ describe.concurrent('OpenRouter generateText per model', () => { `[generateText] OpenRouter ${model_id} failed: ${details}` ); } - }, 45_000); // 15 second timeout per test + }); // 15 second timeout per test } }); From 17c237c1d68e738a427d3414bf3d00cfbfbb1b7f Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 17:47:43 -0400 Subject: [PATCH 42/50] rm timeouts --- packages/tests/provider-smoke/openrouter-stream-text.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/provider-smoke/openrouter-stream-text.test.ts b/packages/tests/provider-smoke/openrouter-stream-text.test.ts index 7253e6694..72d969d1d 100644 --- a/packages/tests/provider-smoke/openrouter-stream-text.test.ts +++ b/packages/tests/provider-smoke/openrouter-stream-text.test.ts @@ -53,6 +53,6 @@ describe.concurrent('OpenAI streamText per model', () => { `[streamText] OpenRouter ${model_id} failed: ${details}` ); } - }, 45000); // 45 second timeout per test + }); // 45 second timeout per test } }); From 28d97515609844d7968256e873c31088cb1a6dda Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Thu, 16 Oct 2025 17:57:08 -0400 Subject: [PATCH 43/50] rm openai timeout --- packages/tests/provider-smoke/openai-generate-text.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/provider-smoke/openai-generate-text.test.ts b/packages/tests/provider-smoke/openai-generate-text.test.ts index 0af11cddc..2cffc16eb 100644 --- a/packages/tests/provider-smoke/openai-generate-text.test.ts +++ b/packages/tests/provider-smoke/openai-generate-text.test.ts @@ -63,6 +63,6 @@ describe.concurrent('OpenAI generateText per model', () => { const details = getApiErrorDetails(err); throw new Error(`[generateText] OpenAI ${model_id} failed: ${details}`); } - }, 15_000); + }); } }); From 5dab8453702b19773ba722d00e4c5d1da5ac5a08 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Fri, 17 Oct 2025 03:48:06 +0530 Subject: [PATCH 44/50] Add Groq provider integration and update SDK dependencies --- packages/sdk/next/src/ai-providers/groq.ts | 10 +++++++++ packages/sdk/next/src/ai-providers/index.ts | 1 + packages/sdk/next/src/index.ts | 2 ++ packages/sdk/ts/package.json | 1 + packages/sdk/ts/src/providers/groq.ts | 23 +++++++++++++++++++++ packages/sdk/ts/src/providers/index.ts | 2 ++ 6 files changed, 39 insertions(+) create mode 100644 packages/sdk/next/src/ai-providers/groq.ts create mode 100644 packages/sdk/ts/src/providers/groq.ts diff --git a/packages/sdk/next/src/ai-providers/groq.ts b/packages/sdk/next/src/ai-providers/groq.ts new file mode 100644 index 000000000..d886f19f2 --- /dev/null +++ b/packages/sdk/next/src/ai-providers/groq.ts @@ -0,0 +1,10 @@ +import { getEchoToken } from '../auth/token-manager'; +import { + createEchoGroq as createEchoGroqBase, + EchoConfig, + GroqProvider, +} from '@merit-systems/echo-typescript-sdk'; + +export function createEchoGroq(config: EchoConfig): GroqProvider { + return createEchoGroqBase(config, async () => getEchoToken(config)); +} diff --git a/packages/sdk/next/src/ai-providers/index.ts b/packages/sdk/next/src/ai-providers/index.ts index fee3f9d6f..b3be66a25 100644 --- a/packages/sdk/next/src/ai-providers/index.ts +++ b/packages/sdk/next/src/ai-providers/index.ts @@ -1,3 +1,4 @@ export * from './anthropic'; export * from './google'; +export * from './groq'; export * from './openai'; diff --git a/packages/sdk/next/src/index.ts b/packages/sdk/next/src/index.ts index 34f82e35a..a8688b81d 100644 --- a/packages/sdk/next/src/index.ts +++ b/packages/sdk/next/src/index.ts @@ -22,6 +22,7 @@ import { handleSignOut, handleSession, } from './auth/oauth-handlers'; +import { createEchoGroq } from 'ai-providers/groq'; /** * Echo SDK for Next.js @@ -112,5 +113,6 @@ export default function Echo(config: EchoConfig): EchoResult { openai: createEchoOpenAI(config), anthropic: createEchoAnthropic(config), google: createEchoGoogle(config), + groq: createEchoGroq(config), }; } diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index 54143b854..8ea75642b 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -57,6 +57,7 @@ "dependencies": { "@ai-sdk/anthropic": "2.0.17", "@ai-sdk/google": "2.0.14", + "@ai-sdk/groq": "2.0.17", "@ai-sdk/openai": "2.0.32", "@openrouter/ai-sdk-provider": "1.2.0", "ai": "5.0.47" diff --git a/packages/sdk/ts/src/providers/groq.ts b/packages/sdk/ts/src/providers/groq.ts new file mode 100644 index 000000000..dda64c775 --- /dev/null +++ b/packages/sdk/ts/src/providers/groq.ts @@ -0,0 +1,23 @@ +import { createGroq as createGroqBase, GroqProvider } from '@ai-sdk/groq'; +import { ROUTER_BASE_URL } from 'config'; +import { EchoConfig } from '../types'; +import { validateAppId } from '../utils/validation'; +import { echoFetch } from './index'; + +export function createEchoGroq( + { appId, baseRouterUrl = ROUTER_BASE_URL }: EchoConfig, + getTokenFn: (appId: string) => Promise, + onInsufficientFunds?: () => void +): GroqProvider { + validateAppId(appId, 'createEchoGroq'); + + return createGroqBase({ + baseURL: baseRouterUrl, + apiKey: 'placeholder_replaced_by_echoFetch', + fetch: echoFetch( + fetch, + async () => await getTokenFn(appId), + onInsufficientFunds + ), + }); +} diff --git a/packages/sdk/ts/src/providers/index.ts b/packages/sdk/ts/src/providers/index.ts index 7b5310e3e..3c7d8a987 100644 --- a/packages/sdk/ts/src/providers/index.ts +++ b/packages/sdk/ts/src/providers/index.ts @@ -1,5 +1,6 @@ export * from './anthropic'; export * from './google'; +export * from './groq'; export * from './openai'; export * from './openrouter'; @@ -57,5 +58,6 @@ export function echoFetch( // re-export the underlying types so that next doesn't need to depend on provider specific types export { type AnthropicProvider } from '@ai-sdk/anthropic'; export { type GoogleGenerativeAIProvider } from '@ai-sdk/google'; +export { type GroqProvider } from '@ai-sdk/groq'; export { type OpenAIProvider } from '@ai-sdk/openai'; export { type OpenRouterProvider } from '@openrouter/ai-sdk-provider'; From 3719252cb06f5c3d8a284c6c089533b00381d0ea Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Fri, 17 Oct 2025 14:13:45 -0400 Subject: [PATCH 45/50] fix sdk build issues + add tests --- .../server/src/services/EchoControlService.ts | 2 +- packages/sdk/next/src/ai-providers/groq.ts | 2 +- .../sdk/next/src/ai-providers/openrouter.ts | 8 +- packages/sdk/next/src/index.ts | 6 +- packages/sdk/next/src/types.ts | 4 + .../react/src/hooks/useEchoModelProviders.ts | 2 + .../provider-smoke/groq-generate-text.test.ts | 40 ++++ .../provider-smoke/groq-stream-text.test.ts | 50 +++++ pnpm-lock.yaml | 201 ++++++++++++++---- 9 files changed, 261 insertions(+), 54 deletions(-) create mode 100644 packages/tests/provider-smoke/groq-generate-text.test.ts create mode 100644 packages/tests/provider-smoke/groq-stream-text.test.ts diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index dca4e6558..8cbe67626 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` + diff --git a/packages/sdk/next/src/ai-providers/groq.ts b/packages/sdk/next/src/ai-providers/groq.ts index d886f19f2..b7270fed7 100644 --- a/packages/sdk/next/src/ai-providers/groq.ts +++ b/packages/sdk/next/src/ai-providers/groq.ts @@ -1,9 +1,9 @@ -import { getEchoToken } from '../auth/token-manager'; import { createEchoGroq as createEchoGroqBase, EchoConfig, GroqProvider, } from '@merit-systems/echo-typescript-sdk'; +import { getEchoToken } from '../auth/token-manager'; export function createEchoGroq(config: EchoConfig): GroqProvider { return createEchoGroqBase(config, async () => getEchoToken(config)); diff --git a/packages/sdk/next/src/ai-providers/openrouter.ts b/packages/sdk/next/src/ai-providers/openrouter.ts index 576d58cab..361f5fb53 100644 --- a/packages/sdk/next/src/ai-providers/openrouter.ts +++ b/packages/sdk/next/src/ai-providers/openrouter.ts @@ -1,10 +1,10 @@ import { getEchoToken } from '../auth/token-manager'; import { - createEchoOpenAI as createEchoOpenAIBase, + createEchoOpenRouter as createEchoOpenRouterBase, EchoConfig, - OpenAIProvider, + OpenRouterProvider, } from '@merit-systems/echo-typescript-sdk'; -export function createEchoOpenRouter(config: EchoConfig): OpenAIProvider { - return createEchoOpenAIBase(config, async () => getEchoToken(config)); +export function createEchoOpenRouter(config: EchoConfig): OpenRouterProvider { + return createEchoOpenRouterBase(config, async () => getEchoToken(config)); } diff --git a/packages/sdk/next/src/index.ts b/packages/sdk/next/src/index.ts index a8688b81d..5b14aee5a 100644 --- a/packages/sdk/next/src/index.ts +++ b/packages/sdk/next/src/index.ts @@ -6,6 +6,8 @@ import { EchoConfig, EchoResult } from './types'; import { createEchoAnthropic } from 'ai-providers/anthropic'; import { createEchoGoogle } from 'ai-providers/google'; import { createEchoOpenAI } from 'ai-providers/openai'; +import { createEchoOpenRouter } from 'ai-providers/openrouter'; +import { createEchoGroq } from 'ai-providers/groq'; import { CreateOauthTokenResponse, @@ -18,11 +20,10 @@ import { handleEchoClientProxy } from 'proxy'; import { handleCallback, handleRefresh, + handleSession, handleSignIn, handleSignOut, - handleSession, } from './auth/oauth-handlers'; -import { createEchoGroq } from 'ai-providers/groq'; /** * Echo SDK for Next.js @@ -114,5 +115,6 @@ export default function Echo(config: EchoConfig): EchoResult { anthropic: createEchoAnthropic(config), google: createEchoGoogle(config), groq: createEchoGroq(config), + openrouter: createEchoOpenRouter(config), }; } diff --git a/packages/sdk/next/src/types.ts b/packages/sdk/next/src/types.ts index 815198059..e47dd6026 100644 --- a/packages/sdk/next/src/types.ts +++ b/packages/sdk/next/src/types.ts @@ -3,6 +3,8 @@ import { GoogleGenerativeAIProvider, OpenAIProvider, CreateOauthTokenResponse, + GroqProvider, + OpenRouterProvider, } from '@merit-systems/echo-typescript-sdk'; import { NextRequest } from 'next/server'; @@ -46,4 +48,6 @@ export type EchoResult = { openai: OpenAIProvider; anthropic: AnthropicProvider; google: GoogleGenerativeAIProvider; + groq: GroqProvider; + openrouter: OpenRouterProvider; }; diff --git a/packages/sdk/react/src/hooks/useEchoModelProviders.ts b/packages/sdk/react/src/hooks/useEchoModelProviders.ts index 67f3521f4..72d412aa2 100644 --- a/packages/sdk/react/src/hooks/useEchoModelProviders.ts +++ b/packages/sdk/react/src/hooks/useEchoModelProviders.ts @@ -1,6 +1,7 @@ import { createEchoAnthropic, createEchoGoogle, + createEchoGroq, createEchoOpenAI, createEchoOpenRouter, } from '@merit-systems/echo-typescript-sdk'; @@ -27,6 +28,7 @@ export const useEchoModelProviders = () => { getToken, onInsufficientFunds ), + groq: createEchoGroq(baseConfig, getToken, onInsufficientFunds), }; }, [getToken, config.appId, config.baseRouterUrl, setIsInsufficientFunds]); }; diff --git a/packages/tests/provider-smoke/groq-generate-text.test.ts b/packages/tests/provider-smoke/groq-generate-text.test.ts new file mode 100644 index 000000000..f77011372 --- /dev/null +++ b/packages/tests/provider-smoke/groq-generate-text.test.ts @@ -0,0 +1,40 @@ +import { + createEchoGoogle, + createEchoGroq, + GeminiModels, +} from '@merit-systems/echo-typescript-sdk'; +import { generateText } from 'ai'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { + assertEnv, + baseRouterUrl, + ECHO_APP_ID, + getApiErrorDetails, + getToken, +} from './test-helpers'; +import { NON_CHAT_MODELS } from 'groq-stream-text.test'; + +beforeAll(assertEnv); + +describe.concurrent('Groq generateText per model', () => { + const groq = createEchoGroq({ appId: ECHO_APP_ID!, baseRouterUrl }, getToken); + + for (const { model_id } of GeminiModels) { + if (NON_CHAT_MODELS.includes(model_id)) { + continue; + } + it(`Groq ${model_id}`, async () => { + try { + const { text } = await generateText({ + model: groq(model_id), + prompt: 'One-word greeting.', + }); + expect(text).toBeDefined(); + expect(text).not.toBe(''); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[generateText] Groq ${model_id} failed: ${details}`); + } + }); + } +}); diff --git a/packages/tests/provider-smoke/groq-stream-text.test.ts b/packages/tests/provider-smoke/groq-stream-text.test.ts new file mode 100644 index 000000000..fa025e9b1 --- /dev/null +++ b/packages/tests/provider-smoke/groq-stream-text.test.ts @@ -0,0 +1,50 @@ +import { + createEchoGoogle, + createEchoGroq, + GeminiModels, +} from '@merit-systems/echo-typescript-sdk'; +import { streamText } from 'ai'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { + ECHO_APP_ID, + assertEnv, + baseRouterUrl, + getApiErrorDetails, + getToken, +} from './test-helpers'; + +beforeAll(assertEnv); + +export const NON_CHAT_MODELS = [ + 'gemini-2.0-flash-preview-image-generation', + 'gemini-2.5-flash-preview-tts', + 'gemini-2.5-pro-preview-tts', + 'gemini-2.0-flash-preview-image-generation', + 'gemini-2.0-flash-exp-image-generation', + 'gemini-2.0-flash-thinking-exp', +]; + +describe.concurrent('Groq streamText per model', () => { + const groq = createEchoGroq({ appId: ECHO_APP_ID!, baseRouterUrl }, getToken); + + for (const { model_id } of GeminiModels) { + if (NON_CHAT_MODELS.includes(model_id)) { + continue; + } + it(`Groq streamText ${model_id}`, async () => { + try { + const { textStream } = streamText({ + model: groq(model_id), + prompt: 'One-word greeting.', + }); + let streamed = ''; + for await (const d of textStream) streamed += d; + expect(streamed).toBeDefined(); + expect(streamed).not.toBe(''); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[generateText] Groq ${model_id} failed: ${details}`); + } + }); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48b839a48..a3d5a1f28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 2.10.0(@prisma/client@6.16.0(prisma@6.16.0(magicast@0.3.5)(typescript@5.9.2))(typescript@5.9.2)) '@coinbase/x402': specifier: ^0.6.4 - version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@hookform/resolvers': specifier: ^5.2.1 version: 5.2.1(react-hook-form@7.62.0(react@19.1.1)) @@ -419,7 +419,7 @@ importers: version: 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@coinbase/x402': specifier: ^0.6.5 - version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@e2b/code-interpreter': specifier: ^2.0.1 version: 2.0.1 @@ -509,7 +509,7 @@ importers: version: 2.7.0 openai: specifier: ^6.2.0 - version: 6.2.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11) + version: 6.2.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11) prisma: specifier: 6.16.0 version: 6.16.0(magicast@0.3.5)(typescript@5.9.2) @@ -533,7 +533,7 @@ importers: version: 3.17.0 x402-express: specifier: ^0.6.5 - version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^4.1.11 version: 4.1.11 @@ -1206,6 +1206,9 @@ importers: '@ai-sdk/google': specifier: 2.0.14 version: 2.0.14(zod@4.1.11) + '@ai-sdk/groq': + specifier: 2.0.17 + version: 2.0.17(zod@4.1.11) '@ai-sdk/openai': specifier: 2.0.32 version: 2.0.32(zod@4.1.11) @@ -1419,6 +1422,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/groq@2.0.17': + resolution: {integrity: sha512-Oh6Fk988KNvqy4w1crBhBQU8JIkfqhxSiYCbBZFqZSeDsagZ8SHsS2ni9+7pq6e0DR/XGp6fDGDFsm+01kmsmg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/openai@2.0.32': resolution: {integrity: sha512-p7giSkCs66Q1qYO/NPYI41CrSg65mcm8R2uAdF86+Y1D1/q4mUrWMyf5UTOJ0bx/z4jIPiNgGDCg2Kabi5zrKQ==} engines: {node: '>=18'} @@ -1431,6 +1440,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.8': + resolution: {integrity: sha512-cDj1iigu7MW2tgAQeBzOiLhjHOUM9vENsgh4oAVitek0d//WdgfPCsKO3euP7m7LyO/j9a1vr/So+BGNdpFXYw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.9': resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==} engines: {node: '>=18'} @@ -11997,6 +12012,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.9(zod@4.1.11) zod: 4.1.11 + '@ai-sdk/groq@2.0.17(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) + zod: 4.1.11 + '@ai-sdk/openai@2.0.32(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12011,6 +12032,13 @@ snapshots: zod: 4.1.11 zod-to-json-schema: 3.24.6(zod@4.1.11) + '@ai-sdk/provider-utils@3.0.8(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.5 + zod: 4.1.11 + '@ai-sdk/provider-utils@3.0.9(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12626,11 +12654,11 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)': + '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) + x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -12666,11 +12694,11 @@ snapshots: - utf-8-validate - ws - '@coinbase/x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -17392,6 +17420,10 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -17400,9 +17432,13 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.2))': + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: @@ -17414,9 +17450,13 @@ snapshots: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(typescript@5.9.2))': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: @@ -17426,9 +17466,9 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(typescript@5.9.2))': + '@solana-program/token@0.5.1(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2) + '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: @@ -17585,6 +17625,31 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17635,7 +17700,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@2.3.0(typescript@5.9.2)': + '@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17648,11 +17713,11 @@ snapshots: '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 @@ -17742,6 +17807,15 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -17768,6 +17842,24 @@ snapshots: '@solana/subscribable': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -17910,6 +18002,23 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17944,7 +18053,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@2.3.0(typescript@5.9.2)': + '@solana/transaction-confirmation@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17952,7 +18061,7 @@ snapshots: '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/promises': 2.3.0(typescript@5.9.2) '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -19028,14 +19137,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.11.2(@types/node@20.19.16)(typescript@5.9.2) - vite: 6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: @@ -19083,7 +19192,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.19.16)(@vitest/ui@3.2.3)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/ui@3.2.3)(jiti@2.5.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.1)(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/utils@3.0.9': dependencies: @@ -21910,7 +22019,7 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -21930,7 +22039,7 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -21954,7 +22063,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -21969,14 +22078,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -21991,7 +22100,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -24926,9 +25035,9 @@ snapshots: ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.11 - openai@6.2.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11): + openai@6.2.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11): optionalDependencies: - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.11 openapi-fetch@0.13.8: @@ -27753,7 +27862,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 @@ -28208,13 +28317,13 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402-express@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-express@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) express: 4.21.2 viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -28457,14 +28566,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10): + x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(typescript@5.9.2)) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(typescript@5.9.2)) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(typescript@5.9.2)) - '@solana/kit': 2.3.0(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 @@ -28501,14 +28610,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 From 46fc77bb6b5f1f346c013cd0a01249c9ca0bc599 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Fri, 17 Oct 2025 14:11:09 -0400 Subject: [PATCH 46/50] lock --- pnpm-lock.yaml | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3d5a1f28..2406ee54d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1206,9 +1206,6 @@ importers: '@ai-sdk/google': specifier: 2.0.14 version: 2.0.14(zod@4.1.11) - '@ai-sdk/groq': - specifier: 2.0.17 - version: 2.0.17(zod@4.1.11) '@ai-sdk/openai': specifier: 2.0.32 version: 2.0.32(zod@4.1.11) @@ -1422,12 +1419,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 - '@ai-sdk/groq@2.0.17': - resolution: {integrity: sha512-Oh6Fk988KNvqy4w1crBhBQU8JIkfqhxSiYCbBZFqZSeDsagZ8SHsS2ni9+7pq6e0DR/XGp6fDGDFsm+01kmsmg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4 - '@ai-sdk/openai@2.0.32': resolution: {integrity: sha512-p7giSkCs66Q1qYO/NPYI41CrSg65mcm8R2uAdF86+Y1D1/q4mUrWMyf5UTOJ0bx/z4jIPiNgGDCg2Kabi5zrKQ==} engines: {node: '>=18'} @@ -1440,12 +1431,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 - '@ai-sdk/provider-utils@3.0.8': - resolution: {integrity: sha512-cDj1iigu7MW2tgAQeBzOiLhjHOUM9vENsgh4oAVitek0d//WdgfPCsKO3euP7m7LyO/j9a1vr/So+BGNdpFXYw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4 - '@ai-sdk/provider-utils@3.0.9': resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==} engines: {node: '>=18'} @@ -12012,12 +11997,6 @@ snapshots: '@ai-sdk/provider-utils': 3.0.9(zod@4.1.11) zod: 4.1.11 - '@ai-sdk/groq@2.0.17(zod@4.1.11)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) - zod: 4.1.11 - '@ai-sdk/openai@2.0.32(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12032,13 +12011,6 @@ snapshots: zod: 4.1.11 zod-to-json-schema: 3.24.6(zod@4.1.11) - '@ai-sdk/provider-utils@3.0.8(zod@4.1.11)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 - eventsource-parser: 3.0.5 - zod: 4.1.11 - '@ai-sdk/provider-utils@3.0.9(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -22019,7 +21991,7 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -22039,7 +22011,7 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -22063,7 +22035,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -22078,14 +22050,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -22100,7 +22072,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 1847e34355717fe95fa2871f99aef4c58081575a Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Fri, 17 Oct 2025 14:15:11 -0400 Subject: [PATCH 47/50] bump versions --- packages/sdk/next/package.json | 2 +- packages/sdk/react/package.json | 2 +- packages/sdk/ts/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/next/package.json b/packages/sdk/next/package.json index 0630525f1..1abeab3cd 100644 --- a/packages/sdk/next/package.json +++ b/packages/sdk/next/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-next-sdk", - "version": "0.0.28", + "version": "0.0.29", "description": "Next.js SDK for Echo", "type": "module", "main": "./dist/index.js", diff --git a/packages/sdk/react/package.json b/packages/sdk/react/package.json index fadb5d46c..320a03cf2 100644 --- a/packages/sdk/react/package.json +++ b/packages/sdk/react/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-react-sdk", - "version": "1.0.37", + "version": "1.0.38", "description": "React SDK for Echo OAuth2 + PKCE authentication and token management", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index 17fa91ede..fa5f3a993 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -1,6 +1,6 @@ { "name": "@merit-systems/echo-typescript-sdk", - "version": "1.0.21", + "version": "1.0.22", "description": "TypeScript SDK for Echo platform", "type": "module", "main": "./dist/index.cjs", From 813fc82f3c6103444a10666ff27fab9064a8ce26 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Fri, 17 Oct 2025 14:16:23 -0400 Subject: [PATCH 48/50] mr lock --- pnpm-lock.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2406ee54d..241148c53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1206,6 +1206,9 @@ importers: '@ai-sdk/google': specifier: 2.0.14 version: 2.0.14(zod@4.1.11) + '@ai-sdk/groq': + specifier: 2.0.17 + version: 2.0.17(zod@4.1.11) '@ai-sdk/openai': specifier: 2.0.32 version: 2.0.32(zod@4.1.11) @@ -1419,6 +1422,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/groq@2.0.17': + resolution: {integrity: sha512-Oh6Fk988KNvqy4w1crBhBQU8JIkfqhxSiYCbBZFqZSeDsagZ8SHsS2ni9+7pq6e0DR/XGp6fDGDFsm+01kmsmg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/openai@2.0.32': resolution: {integrity: sha512-p7giSkCs66Q1qYO/NPYI41CrSg65mcm8R2uAdF86+Y1D1/q4mUrWMyf5UTOJ0bx/z4jIPiNgGDCg2Kabi5zrKQ==} engines: {node: '>=18'} @@ -1431,6 +1440,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.8': + resolution: {integrity: sha512-cDj1iigu7MW2tgAQeBzOiLhjHOUM9vENsgh4oAVitek0d//WdgfPCsKO3euP7m7LyO/j9a1vr/So+BGNdpFXYw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.9': resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==} engines: {node: '>=18'} @@ -11997,6 +12012,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.9(zod@4.1.11) zod: 4.1.11 + '@ai-sdk/groq@2.0.17(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) + zod: 4.1.11 + '@ai-sdk/openai@2.0.32(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12011,6 +12032,13 @@ snapshots: zod: 4.1.11 zod-to-json-schema: 3.24.6(zod@4.1.11) + '@ai-sdk/provider-utils@3.0.8(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.5 + zod: 4.1.11 + '@ai-sdk/provider-utils@3.0.9(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 From f2202a89193802af431dbf3d08763a528e52a9fa Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Fri, 17 Oct 2025 15:06:32 -0400 Subject: [PATCH 49/50] update groq models with correct names --- packages/app/server/src/handlers.ts | 2 +- packages/app/server/src/resources/e2b/e2b.ts | 53 ++++----- .../app/server/src/resources/e2b/prices.ts | 2 +- .../app/server/src/resources/e2b/route.ts | 9 +- .../app/server/src/resources/e2b/types.ts | 37 ++++--- .../src/resources/tavily/crawl/route.ts | 3 +- .../src/resources/tavily/crawl/tavily.ts | 31 ++++-- .../src/resources/tavily/crawl/types.ts | 1 - .../src/resources/tavily/extract/route.ts | 3 +- .../src/resources/tavily/extract/tavily.ts | 6 +- .../src/resources/tavily/extract/types.ts | 1 - .../app/server/src/resources/tavily/prices.ts | 2 +- .../src/resources/tavily/search/route.ts | 10 +- packages/sdk/ts/package.json | 4 +- packages/sdk/ts/scripts/update-groq-models.ts | 71 ++++++++++++ packages/sdk/ts/scripts/update-models.md | 2 + .../sdk/ts/src/supported-models/chat/groq.ts | 102 ++++++++---------- .../src/supported-models/chat/openrouter.ts | 32 +++++- .../provider-smoke/groq-generate-text.test.ts | 8 +- .../provider-smoke/groq-stream-text.test.ts | 16 +-- pnpm-lock.yaml | 24 ++++- 21 files changed, 264 insertions(+), 155 deletions(-) create mode 100755 packages/sdk/ts/scripts/update-groq-models.ts diff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts index 66579ba6a..0c1ae3e1a 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -30,7 +30,7 @@ export async function refund( payload: ExactEvmPayload ) { try { - const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); + const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); const authPayload = payload.authorization; await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); } catch (error) { diff --git a/packages/app/server/src/resources/e2b/e2b.ts b/packages/app/server/src/resources/e2b/e2b.ts index 218c1d46b..feb1bbb86 100644 --- a/packages/app/server/src/resources/e2b/e2b.ts +++ b/packages/app/server/src/resources/e2b/e2b.ts @@ -1,15 +1,17 @@ -import { Sandbox } from "@e2b/code-interpreter"; +import { Sandbox } from '@e2b/code-interpreter'; import dotenv from 'dotenv'; -import { E2BExecuteOutput, E2BExecuteInput } from "./types"; -import { DEFAULT_VCPU_COUNT, PRICE_PER_VCPU_PER_SECOND } from "./prices"; -import { Decimal } from "@prisma/client/runtime/library"; -import { Transaction } from "../../types"; +import { E2BExecuteOutput, E2BExecuteInput } from './types'; +import { DEFAULT_VCPU_COUNT, PRICE_PER_VCPU_PER_SECOND } from './prices'; +import { Decimal } from '@prisma/client/runtime/library'; +import { Transaction } from '../../types'; import { HttpError } from 'errors/http'; dotenv.config(); export const calculateE2BExecuteCost = (): Decimal => { const estimatedDurationSeconds = 10; - return new Decimal((estimatedDurationSeconds) * PRICE_PER_VCPU_PER_SECOND * DEFAULT_VCPU_COUNT); + return new Decimal( + estimatedDurationSeconds * PRICE_PER_VCPU_PER_SECOND * DEFAULT_VCPU_COUNT + ); }; export const createE2BTransaction = ( @@ -32,41 +34,40 @@ export const createE2BTransaction = ( }; }; -export const e2bExecutePythonSnippet = async (snippet: string): Promise => { +export const e2bExecutePythonSnippet = async ( + snippet: string +): Promise => { if (!process.env.E2B_API_KEY) { - throw new Error( - "E2B_API_KEY environment variable is required but not set", - ); + throw new Error('E2B_API_KEY environment variable is required but not set'); } try { - const startTime = performance.now(); const sandbox = await Sandbox.create({ - apiKey: process.env.E2B_API_KEY, + apiKey: process.env.E2B_API_KEY, }); - const { results, logs, error, executionCount } = await sandbox.runCode(snippet, { + const { results, logs, error, executionCount } = await sandbox.runCode( + snippet, + { timeoutMs: 10000, requestTimeoutMs: 15000, - }); + } + ); await sandbox.kill(); const endTime = performance.now(); const durationMs = endTime - startTime; const duration = durationMs / 1000; const cost = duration * PRICE_PER_VCPU_PER_SECOND * DEFAULT_VCPU_COUNT; return { - results: results, - logs: logs, - error: error, - executionCount: executionCount, - cost: cost, - sandboxId: sandbox.sandboxId, - duration: duration, + results: results, + logs: logs, + error: error, + executionCount: executionCount, + cost: cost, + sandboxId: sandbox.sandboxId, + duration: duration, }; } catch (error) { const errorText = error instanceof Error ? error.message : 'Unknown error'; - throw new HttpError( - 400, - `E2B API request failed: ${errorText}` - ); + throw new HttpError(400, `E2B API request failed: ${errorText}`); } -}; \ No newline at end of file +}; diff --git a/packages/app/server/src/resources/e2b/prices.ts b/packages/app/server/src/resources/e2b/prices.ts index f57fe4b67..038e7deb2 100644 --- a/packages/app/server/src/resources/e2b/prices.ts +++ b/packages/app/server/src/resources/e2b/prices.ts @@ -1,3 +1,3 @@ export const PRICE_PER_VCPU_PER_SECOND = 0.000014; -export const DEFAULT_VCPU_COUNT = 2; \ No newline at end of file +export const DEFAULT_VCPU_COUNT = 2; diff --git a/packages/app/server/src/resources/e2b/route.ts b/packages/app/server/src/resources/e2b/route.ts index 9319e7f5a..9fcc3ef51 100644 --- a/packages/app/server/src/resources/e2b/route.ts +++ b/packages/app/server/src/resources/e2b/route.ts @@ -1,17 +1,20 @@ import { Request, Response } from 'express'; import { Decimal } from '@prisma/client/runtime/library'; import { E2BExecuteInputSchema } from './types'; -import { calculateE2BExecuteCost, e2bExecutePythonSnippet, createE2BTransaction } from './e2b'; +import { + calculateE2BExecuteCost, + e2bExecutePythonSnippet, + createE2BTransaction, +} from './e2b'; import { handleResourceRequestWithErrorHandling } from '../handler'; export async function e2bExecuteRoute(req: Request, res: Response) { return handleResourceRequestWithErrorHandling(req, res, { inputSchema: E2BExecuteInputSchema, calculateMaxCost: () => calculateE2BExecuteCost(), - executeResource: (input) => e2bExecutePythonSnippet(input.snippet), + executeResource: input => e2bExecutePythonSnippet(input.snippet), calculateActualCost: (_input, output) => new Decimal(output.cost), createTransaction: createE2BTransaction, errorMessage: 'Error executing e2b code', }); } - diff --git a/packages/app/server/src/resources/e2b/types.ts b/packages/app/server/src/resources/e2b/types.ts index 8b96cc5f3..16edb05f2 100644 --- a/packages/app/server/src/resources/e2b/types.ts +++ b/packages/app/server/src/resources/e2b/types.ts @@ -1,33 +1,33 @@ -import { z } from "zod"; +import { z } from 'zod'; // Input schema export const E2BExecuteInputSchema = z.object({ - snippet: z.string().min(1, "Code snippet cannot be empty"), + snippet: z.string().min(1, 'Code snippet cannot be empty'), }); export type E2BExecuteInput = z.infer; // Chart type enums export const ChartTypeSchema = z.enum([ - "line", - "scatter", - "bar", - "pie", - "box_and_whisker", - "superchart", - "unknown", + 'line', + 'scatter', + 'bar', + 'pie', + 'box_and_whisker', + 'superchart', + 'unknown', ]); export const ScaleTypeSchema = z.enum([ - "linear", - "datetime", - "categorical", - "log", - "symlog", - "logit", - "function", - "functionlog", - "asinh", + 'linear', + 'datetime', + 'categorical', + 'log', + 'symlog', + 'logit', + 'function', + 'functionlog', + 'asinh', ]); // Result schema @@ -80,4 +80,3 @@ export const E2BExecuteOutputSchema = z.object({ }); export type E2BExecuteOutput = z.infer; - diff --git a/packages/app/server/src/resources/tavily/crawl/route.ts b/packages/app/server/src/resources/tavily/crawl/route.ts index 84d646225..dc7d21732 100644 --- a/packages/app/server/src/resources/tavily/crawl/route.ts +++ b/packages/app/server/src/resources/tavily/crawl/route.ts @@ -11,11 +11,10 @@ import { handleResourceRequestWithErrorHandling } from '../../handler'; export async function tavilyCrawlRoute(req: Request, res: Response) { return handleResourceRequestWithErrorHandling(req, res, { inputSchema: TavilyCrawlInputSchema, - calculateMaxCost: (input) => calculateTavilyCrawlMaxCost(input), + calculateMaxCost: input => calculateTavilyCrawlMaxCost(input), executeResource: tavilyCrawl, calculateActualCost: calculateTavilyCrawlActualCost, createTransaction: createTavilyTransaction, errorMessage: 'Error crawling tavily', }); } - diff --git a/packages/app/server/src/resources/tavily/crawl/tavily.ts b/packages/app/server/src/resources/tavily/crawl/tavily.ts index 95ad0a1ab..3cbeac46c 100644 --- a/packages/app/server/src/resources/tavily/crawl/tavily.ts +++ b/packages/app/server/src/resources/tavily/crawl/tavily.ts @@ -1,5 +1,9 @@ import { Decimal } from '@prisma/client/runtime/library'; -import { CREDIT_PRICE, TAVILY_MAP_PRICING, TAVILY_EXTRACT_PRICING } from '../prices'; +import { + CREDIT_PRICE, + TAVILY_MAP_PRICING, + TAVILY_EXTRACT_PRICING, +} from '../prices'; import { TavilyCrawlInput, TavilyCrawlOutput, @@ -21,13 +25,15 @@ export const calculateTavilyCrawlMaxCost = ( const extractDepth = input.extract_depth ?? 'basic'; // Mapping cost - const mapPricing = hasInstructions - ? TAVILY_MAP_PRICING.withInstructions + const mapPricing = hasInstructions + ? TAVILY_MAP_PRICING.withInstructions : TAVILY_MAP_PRICING.regular; - const mapCredits = Math.ceil(maxPages / mapPricing.pagesPerCredit) * mapPricing.creditsPerUnit; + const mapCredits = + Math.ceil(maxPages / mapPricing.pagesPerCredit) * mapPricing.creditsPerUnit; // Extraction cost - const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[extractDepth]; + const { creditsPerUnit, urlsPerCredit } = + TAVILY_EXTRACT_PRICING[extractDepth]; const extractCredits = Math.ceil(maxPages / urlsPerCredit) * creditsPerUnit; const totalCredits = mapCredits + extractCredits; @@ -43,14 +49,18 @@ export const calculateTavilyCrawlActualCost = ( const extractDepth = input.extract_depth ?? 'basic'; // Mapping cost - const mapPricing = hasInstructions - ? TAVILY_MAP_PRICING.withInstructions + const mapPricing = hasInstructions + ? TAVILY_MAP_PRICING.withInstructions : TAVILY_MAP_PRICING.regular; - const mapCredits = Math.ceil(successfulPages / mapPricing.pagesPerCredit) * mapPricing.creditsPerUnit; + const mapCredits = + Math.ceil(successfulPages / mapPricing.pagesPerCredit) * + mapPricing.creditsPerUnit; // Extraction cost - const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[extractDepth]; - const extractCredits = Math.ceil(successfulPages / urlsPerCredit) * creditsPerUnit; + const { creditsPerUnit, urlsPerCredit } = + TAVILY_EXTRACT_PRICING[extractDepth]; + const extractCredits = + Math.ceil(successfulPages / urlsPerCredit) * creditsPerUnit; const totalCredits = mapCredits + extractCredits; return new Decimal(totalCredits).mul(CREDIT_PRICE); @@ -101,4 +111,3 @@ export async function tavilyCrawl( const data = await response.json(); return TavilyCrawlOutputSchema.parse(data); } - diff --git a/packages/app/server/src/resources/tavily/crawl/types.ts b/packages/app/server/src/resources/tavily/crawl/types.ts index d091aae0d..4d86fb0b6 100644 --- a/packages/app/server/src/resources/tavily/crawl/types.ts +++ b/packages/app/server/src/resources/tavily/crawl/types.ts @@ -35,4 +35,3 @@ export const TavilyCrawlOutputSchema = z.object({ }); export type TavilyCrawlOutput = z.infer; - diff --git a/packages/app/server/src/resources/tavily/extract/route.ts b/packages/app/server/src/resources/tavily/extract/route.ts index 25f0b5545..c59a37eed 100644 --- a/packages/app/server/src/resources/tavily/extract/route.ts +++ b/packages/app/server/src/resources/tavily/extract/route.ts @@ -11,11 +11,10 @@ import { handleResourceRequestWithErrorHandling } from '../../handler'; export async function tavilyExtractRoute(req: Request, res: Response) { return handleResourceRequestWithErrorHandling(req, res, { inputSchema: TavilyExtractInputSchema, - calculateMaxCost: (input) => calculateTavilyExtractMaxCost(input), + calculateMaxCost: input => calculateTavilyExtractMaxCost(input), executeResource: tavilyExtract, calculateActualCost: calculateTavilyExtractActualCost, createTransaction: createTavilyTransaction, errorMessage: 'Error extracting tavily', }); } - diff --git a/packages/app/server/src/resources/tavily/extract/tavily.ts b/packages/app/server/src/resources/tavily/extract/tavily.ts index 971c207c2..f2c2aa8d3 100644 --- a/packages/app/server/src/resources/tavily/extract/tavily.ts +++ b/packages/app/server/src/resources/tavily/extract/tavily.ts @@ -33,7 +33,8 @@ export const calculateTavilyExtractActualCost = ( const { creditsPerUnit, urlsPerCredit } = TAVILY_EXTRACT_PRICING[depth]; // Calculate actual cost based on successful URLs - const credits = Math.ceil(successfulUrlCount / urlsPerCredit) * creditsPerUnit; + const credits = + Math.ceil(successfulUrlCount / urlsPerCredit) * creditsPerUnit; return new Decimal(credits).mul(CREDIT_PRICE); }; @@ -59,8 +60,6 @@ export const createTavilyTransaction = ( const TAVILY_API_KEY = process.env.TAVILY_API_KEY; - - export async function tavilyExtract( input: TavilyExtractInput ): Promise { @@ -84,4 +83,3 @@ export async function tavilyExtract( const data = await response.json(); return TavilyExtractOutputSchema.parse(data); } - diff --git a/packages/app/server/src/resources/tavily/extract/types.ts b/packages/app/server/src/resources/tavily/extract/types.ts index aa7358000..5b86549ce 100644 --- a/packages/app/server/src/resources/tavily/extract/types.ts +++ b/packages/app/server/src/resources/tavily/extract/types.ts @@ -33,4 +33,3 @@ export const TavilyExtractOutputSchema = z.object({ }); export type TavilyExtractOutput = z.infer; - diff --git a/packages/app/server/src/resources/tavily/prices.ts b/packages/app/server/src/resources/tavily/prices.ts index e27e12f6b..187f40c73 100644 --- a/packages/app/server/src/resources/tavily/prices.ts +++ b/packages/app/server/src/resources/tavily/prices.ts @@ -28,4 +28,4 @@ export const TAVILY_MAP_PRICING = { creditsPerUnit: 2, pagesPerCredit: 10, // Every 10 successful pages with instructions cost 2 credits }, -} as const; \ No newline at end of file +} as const; diff --git a/packages/app/server/src/resources/tavily/search/route.ts b/packages/app/server/src/resources/tavily/search/route.ts index ea33c96e8..12ba059a5 100644 --- a/packages/app/server/src/resources/tavily/search/route.ts +++ b/packages/app/server/src/resources/tavily/search/route.ts @@ -1,14 +1,18 @@ import { Request, Response } from 'express'; import { TavilySearchInputSchema } from './types'; -import { calculateTavilySearchCost, tavilySearch, createTavilyTransaction } from './tavily'; +import { + calculateTavilySearchCost, + tavilySearch, + createTavilyTransaction, +} from './tavily'; import { handleResourceRequestWithErrorHandling } from '../../handler'; export async function tavilySearchRoute(req: Request, res: Response) { return handleResourceRequestWithErrorHandling(req, res, { inputSchema: TavilySearchInputSchema, - calculateMaxCost: (input) => calculateTavilySearchCost(input), + calculateMaxCost: input => calculateTavilySearchCost(input), executeResource: tavilySearch, - calculateActualCost: (input) => calculateTavilySearchCost(input), + calculateActualCost: input => calculateTavilySearchCost(input), createTransaction: createTavilyTransaction, errorMessage: 'Error searching tavily', }); diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index fa5f3a993..93ea1657a 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -28,7 +28,8 @@ "update-models:anthropic": "tsx scripts/update-anthropic-models.ts", "update-models:gemini": "tsx scripts/update-gemini-models.ts", "update-models:openrouter": "tsx scripts/update-openrouter-models.ts", - "update-all-models": "pnpm run update-models:openai && pnpm run update-models:anthropic && pnpm run update-models:gemini && pnpm run update-models:openrouter", + "update-models:groq": "tsx scripts/update-groq-models.ts", + "update-all-models": "pnpm run update-models:openai && pnpm run update-models:anthropic && pnpm run update-models:gemini && pnpm run update-models:openrouter && pnpm run update-models:groq", "prepublishOnly": "pnpm run build" }, "keywords": [ @@ -46,6 +47,7 @@ "@typescript-eslint/parser": "^8.34.1", "dotenv": "^16.5.0", "eslint": "^9.29.0", + "groq-sdk": "^0.33.0", "tsup": "^8.5.0", "tsx": "^4.19.2", "typescript": "^5.8.3", diff --git a/packages/sdk/ts/scripts/update-groq-models.ts b/packages/sdk/ts/scripts/update-groq-models.ts new file mode 100755 index 000000000..c9599bc89 --- /dev/null +++ b/packages/sdk/ts/scripts/update-groq-models.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +// -> Get all model slugs from Groq's API directly +// Note: Groq pricing is not available in AI Gateway, so pricing must be manually updated +// from https://console.groq.com/docs/models + +import Groq from 'groq-sdk'; + +async function fetchGroqModels(): Promise { + const apiKey = process.env.GROQ_API_KEY; + if (!apiKey) { + throw new Error('GROQ_API_KEY environment variable is required'); + } + + try { + console.log('🔄 Fetching models from Groq API...\n'); + + const groq = new Groq({ apiKey }); + const response = await groq.models.list(); + + console.log(`🔍 Found ${response.data.length} total models from Groq API`); + + // Filter for language models (exclude embeddings, audio, etc.) + const languageModels = response.data.filter(model => { + const isNotEmbedding = !model.id.includes('embedding'); + const isNotAudio = + !model.id.includes('whisper') && + !model.id.includes('tts') && + !model.id.includes('audio') && + !model.id.includes('playai'); + const isNotModeration = !model.id.includes('moderation'); + const isNotSystem = !model.id.includes('compound'); + + return isNotEmbedding && isNotAudio && isNotModeration && isNotSystem; + }); + + console.log(`📝 Filtered to ${languageModels.length} language models:\n`); + + // Group by production vs preview (simplified heuristic) + const productionModels = languageModels.filter( + m => !m.id.includes('llama-4') && !m.id.includes('prompt-guard') + ); + const previewModels = languageModels.filter( + m => m.id.includes('llama-4') || m.id.includes('prompt-guard') + ); + + console.log('📦 Production Models:'); + productionModels.forEach(model => { + console.log(` - ${model.id}`); + }); + + console.log('\n🔬 Preview Models:'); + previewModels.forEach(model => { + console.log(` - ${model.id}`); + }); + + console.log('\n✅ Model list fetched successfully!'); + console.log('\n⚠️ Note: Pricing must be manually updated from:'); + console.log(' https://console.groq.com/docs/models'); + console.log('\n📝 Update the file: src/supported-models/chat/groq.ts'); + } catch (error) { + console.error('❌ Error fetching models from Groq API:', error); + throw error; + } +} + +// Run the script +fetchGroqModels().catch(error => { + console.error('❌ Unexpected error:', error); + process.exit(1); +}); diff --git a/packages/sdk/ts/scripts/update-models.md b/packages/sdk/ts/scripts/update-models.md index d917593a0..c1e9ff619 100644 --- a/packages/sdk/ts/scripts/update-models.md +++ b/packages/sdk/ts/scripts/update-models.md @@ -10,6 +10,7 @@ pnpm run update-models:openai pnpm run update-models:anthropic pnpm run update-models:gemini pnpm run update-models:openrouter +pnpm run update-models:groq # Update all providers at once pnpm run update-all-models @@ -23,6 +24,7 @@ Set environment variables for provider API keys: - `ANTHROPIC_API_KEY` - `GOOGLE_GEMINI_API_KEY` - `OPENROUTER_API_KEY` +- `GROQ_API_KEY` ## What it does diff --git a/packages/sdk/ts/src/supported-models/chat/groq.ts b/packages/sdk/ts/src/supported-models/chat/groq.ts index 73474ba4f..7447e8246 100644 --- a/packages/sdk/ts/src/supported-models/chat/groq.ts +++ b/packages/sdk/ts/src/supported-models/chat/groq.ts @@ -1,98 +1,88 @@ import { SupportedModel } from '../types'; // Groq model IDs +// Pricing sourced from: https://console.groq.com/docs/models +// Last updated: 2025-10-17 export type GroqModel = - | 'llama3-8b-8192' - | 'llama3-70b-8192' - | 'mixtral-8x7b-32768' - | 'gemma2-9b-it' | 'llama-3.1-8b-instant' | 'llama-3.3-70b-versatile' - | 'llama-4-scout' - | 'llama-4-maverick' - | 'llama-guard-4-12b' - | 'qwen3-32b' - | 'gpt-oss-20b' - | 'gpt-oss-120b' - | 'kimi-k2-0905-1t'; + | 'meta-llama/llama-guard-4-12b' + | 'openai/gpt-oss-120b' + | 'openai/gpt-oss-20b' + | 'meta-llama/llama-4-maverick-17b-128e-instruct' + | 'meta-llama/llama-4-scout-17b-16e-instruct' + | 'meta-llama/llama-prompt-guard-2-22m' + | 'meta-llama/llama-prompt-guard-2-86m' + | 'moonshotai/kimi-k2-instruct-0905' + | 'qwen/qwen3-32b'; export const GroqModels: SupportedModel[] = [ + // Production Models { - model_id: 'llama3-8b-8192', - input_cost_per_token: 0.00000005, - output_cost_per_token: 0.00000008, - provider: 'Groq', - }, - { - model_id: 'llama3-70b-8192', - input_cost_per_token: 0.00000027, - output_cost_per_token: 0.00000027, - provider: 'Groq', - }, - { - model_id: 'mixtral-8x7b-32768', - input_cost_per_token: 0.00000027, - output_cost_per_token: 0.00000027, + model_id: 'llama-3.1-8b-instant', + input_cost_per_token: 0.00000005, // $0.05 per 1M tokens + output_cost_per_token: 0.00000008, // $0.08 per 1M tokens provider: 'Groq', }, { - model_id: 'gemma2-9b-it', - input_cost_per_token: 0.00000007, - output_cost_per_token: 0.00000007, + model_id: 'llama-3.3-70b-versatile', + input_cost_per_token: 0.00000059, // $0.59 per 1M tokens + output_cost_per_token: 0.00000079, // $0.79 per 1M tokens provider: 'Groq', }, { - model_id: 'llama-3.1-8b-instant', - input_cost_per_token: 0.00000005, - output_cost_per_token: 0.00000008, + model_id: 'meta-llama/llama-guard-4-12b', + input_cost_per_token: 0.0000002, // $0.20 per 1M tokens + output_cost_per_token: 0.0000002, // $0.20 per 1M tokens provider: 'Groq', }, { - model_id: 'llama-3.3-70b-versatile', - input_cost_per_token: 0.00000059, - output_cost_per_token: 0.00000079, + model_id: 'openai/gpt-oss-120b', + input_cost_per_token: 0.00000015, // $0.15 per 1M tokens + output_cost_per_token: 0.0000006, // $0.60 per 1M tokens provider: 'Groq', }, { - model_id: 'llama-4-scout', - input_cost_per_token: 0.00000011, - output_cost_per_token: 0.00000034, + model_id: 'openai/gpt-oss-20b', + input_cost_per_token: 0.000000075, // $0.075 per 1M tokens + output_cost_per_token: 0.0000003, // $0.30 per 1M tokens provider: 'Groq', }, + // Preview Models { - model_id: 'llama-4-maverick', - input_cost_per_token: 0.0000002, - output_cost_per_token: 0.0000006, + model_id: 'meta-llama/llama-4-maverick-17b-128e-instruct', + input_cost_per_token: 0.0000002, // $0.20 per 1M tokens + output_cost_per_token: 0.0000006, // $0.60 per 1M tokens provider: 'Groq', }, { - model_id: 'llama-guard-4-12b', - input_cost_per_token: 0.0000002, - output_cost_per_token: 0.0000002, + model_id: 'meta-llama/llama-4-scout-17b-16e-instruct', + input_cost_per_token: 0.00000011, // $0.11 per 1M tokens + output_cost_per_token: 0.00000034, // $0.34 per 1M tokens provider: 'Groq', }, { - model_id: 'qwen3-32b', - input_cost_per_token: 0.00000029, - output_cost_per_token: 0.00000059, + model_id: 'meta-llama/llama-prompt-guard-2-22m', + input_cost_per_token: 0.00000003, // $0.03 per 1M tokens + output_cost_per_token: 0.00000003, // $0.03 per 1M tokens provider: 'Groq', }, { - model_id: 'gpt-oss-20b', - input_cost_per_token: 0.000000075, - output_cost_per_token: 0.0000003, + model_id: 'meta-llama/llama-prompt-guard-2-86m', + input_cost_per_token: 0.00000004, // $0.04 per 1M tokens + output_cost_per_token: 0.00000004, // $0.04 per 1M tokens provider: 'Groq', }, { - model_id: 'gpt-oss-120b', - input_cost_per_token: 0.00000015, - output_cost_per_token: 0.0000006, + model_id: 'moonshotai/kimi-k2-instruct-0905', + input_cost_per_token: 0.000001, // $1.00 per 1M tokens + output_cost_per_token: 0.000003, // $3.00 per 1M tokens provider: 'Groq', }, { - model_id: 'kimi-k2-0905-1t', - input_cost_per_token: 0.000001, - output_cost_per_token: 0.000003, + model_id: 'qwen/qwen3-32b', + input_cost_per_token: 0.00000029, // $0.29 per 1M tokens + output_cost_per_token: 0.00000059, // $0.59 per 1M tokens provider: 'Groq', }, ]; diff --git a/packages/sdk/ts/src/supported-models/chat/openrouter.ts b/packages/sdk/ts/src/supported-models/chat/openrouter.ts index 69b98053c..0c07dbd2d 100644 --- a/packages/sdk/ts/src/supported-models/chat/openrouter.ts +++ b/packages/sdk/ts/src/supported-models/chat/openrouter.ts @@ -45,6 +45,8 @@ export type OpenRouterModel = | 'cohere/command-r7b-12-2024' | 'deepcogito/cogito-v2-preview-deepseek-671b' | 'deepcogito/cogito-v2-preview-llama-109b-moe' + | 'deepcogito/cogito-v2-preview-llama-405b' + | 'deepcogito/cogito-v2-preview-llama-70b' | 'deepseek/deepseek-chat' | 'deepseek/deepseek-chat-v3-0324' | 'deepseek/deepseek-chat-v3.1' @@ -77,6 +79,8 @@ export type OpenRouterModel = | 'gryphe/mythomax-l2-13b' | 'inception/mercury' | 'inception/mercury-coder' + | 'inclusionai/ling-1t' + | 'inclusionai/ring-1t' | 'inflection/inflection-3-pi' | 'inflection/inflection-3-productivity' | 'liquid/lfm-3b' @@ -531,6 +535,18 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 5.9e-7, provider: 'OpenRouter', }, + { + model_id: 'deepcogito/cogito-v2-preview-llama-405b', + input_cost_per_token: 0.0000035, + output_cost_per_token: 0.0000035, + provider: 'OpenRouter', + }, + { + model_id: 'deepcogito/cogito-v2-preview-llama-70b', + input_cost_per_token: 8.8e-7, + output_cost_per_token: 8.8e-7, + provider: 'OpenRouter', + }, { model_id: 'deepseek/deepseek-chat', input_cost_per_token: 3e-7, @@ -723,6 +739,18 @@ export const OpenRouterModels: SupportedModel[] = [ output_cost_per_token: 0.000001, provider: 'OpenRouter', }, + { + model_id: 'inclusionai/ling-1t', + input_cost_per_token: 4e-7, + output_cost_per_token: 0.000002, + provider: 'OpenRouter', + }, + { + model_id: 'inclusionai/ring-1t', + input_cost_per_token: 5.7e-7, + output_cost_per_token: 0.00000228, + provider: 'OpenRouter', + }, { model_id: 'inflection/inflection-3-pi', input_cost_per_token: 0.0000025, @@ -1661,8 +1689,8 @@ export const OpenRouterModels: SupportedModel[] = [ }, { model_id: 'qwen/qwen3-vl-235b-a22b-thinking', - input_cost_per_token: 4.5e-7, - output_cost_per_token: 0.0000035, + input_cost_per_token: 3e-7, + output_cost_per_token: 0.0000012, provider: 'OpenRouter', }, { diff --git a/packages/tests/provider-smoke/groq-generate-text.test.ts b/packages/tests/provider-smoke/groq-generate-text.test.ts index f77011372..4e6e0cf46 100644 --- a/packages/tests/provider-smoke/groq-generate-text.test.ts +++ b/packages/tests/provider-smoke/groq-generate-text.test.ts @@ -1,8 +1,4 @@ -import { - createEchoGoogle, - createEchoGroq, - GeminiModels, -} from '@merit-systems/echo-typescript-sdk'; +import { createEchoGroq, GroqModels } from '@merit-systems/echo-typescript-sdk'; import { generateText } from 'ai'; import { beforeAll, describe, expect, it } from 'vitest'; import { @@ -19,7 +15,7 @@ beforeAll(assertEnv); describe.concurrent('Groq generateText per model', () => { const groq = createEchoGroq({ appId: ECHO_APP_ID!, baseRouterUrl }, getToken); - for (const { model_id } of GeminiModels) { + for (const { model_id } of GroqModels) { if (NON_CHAT_MODELS.includes(model_id)) { continue; } diff --git a/packages/tests/provider-smoke/groq-stream-text.test.ts b/packages/tests/provider-smoke/groq-stream-text.test.ts index fa025e9b1..05e663c17 100644 --- a/packages/tests/provider-smoke/groq-stream-text.test.ts +++ b/packages/tests/provider-smoke/groq-stream-text.test.ts @@ -1,8 +1,4 @@ -import { - createEchoGoogle, - createEchoGroq, - GeminiModels, -} from '@merit-systems/echo-typescript-sdk'; +import { createEchoGroq, GroqModels } from '@merit-systems/echo-typescript-sdk'; import { streamText } from 'ai'; import { beforeAll, describe, expect, it } from 'vitest'; import { @@ -16,18 +12,14 @@ import { beforeAll(assertEnv); export const NON_CHAT_MODELS = [ - 'gemini-2.0-flash-preview-image-generation', - 'gemini-2.5-flash-preview-tts', - 'gemini-2.5-pro-preview-tts', - 'gemini-2.0-flash-preview-image-generation', - 'gemini-2.0-flash-exp-image-generation', - 'gemini-2.0-flash-thinking-exp', + 'meta-llama/llama-prompt-guard-2-22m', + 'meta-llama/llama-prompt-guard-2-86m', ]; describe.concurrent('Groq streamText per model', () => { const groq = createEchoGroq({ appId: ECHO_APP_ID!, baseRouterUrl }, getToken); - for (const { model_id } of GeminiModels) { + for (const { model_id } of GroqModels) { if (NON_CHAT_MODELS.includes(model_id)) { continue; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 241148c53..bd9d05738 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1237,6 +1237,9 @@ importers: eslint: specifier: ^9.29.0 version: 9.35.0(jiti@2.5.1) + groq-sdk: + specifier: ^0.33.0 + version: 0.33.0 tsup: specifier: ^8.5.0 version: 8.5.0(@microsoft/api-extractor@7.52.8(@types/node@24.3.1))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.0) @@ -7991,6 +7994,9 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + groq-sdk@0.33.0: + resolution: {integrity: sha512-wb7NrBq7LZDDhDPSpuAd9LpZ0MNjmWKGLfybYfjY3r63mSpfiP8+GQZQcSDJcX+jIMzSm+SwzxModDyVZ2T66Q==} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -19137,14 +19143,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.11.2(@types/node@20.19.16)(typescript@5.9.2) - vite: 6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vite: 6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: @@ -22988,6 +22994,18 @@ snapshots: graphql@16.11.0: {} + groq-sdk@0.33.0: + dependencies: + '@types/node': 18.19.112 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -27862,7 +27880,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 From 395d070fdaf72822d3ecfda1d0a107d8400d2845 Mon Sep 17 00:00:00 2001 From: Ryan Sproule Date: Fri, 17 Oct 2025 15:16:58 -0400 Subject: [PATCH 50/50] gdmmit alv --- packages/app/server/src/services/EchoControlService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index 8cbe67626..dca4e6558 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` +