diff --git a/.github/workflows/provider-smoke-tests.yml b/.github/workflows/provider-smoke-tests.yml new file mode 100644 index 000000000..2869ff26c --- /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: 40 + + 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=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 + 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 (waited 15 minutes)" + 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 diff --git a/Dockerfile.railway b/Dockerfile.railway index ee055678e..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 @@ -103,7 +107,7 @@ WORKDIR /app/packages/app/server RUN pnpm run copy-prisma # Step 4: Install production dependencies only -RUN pnpm install +RUN pnpm install --prod # Expose the port that echo-server runs on EXPOSE 3069 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/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 3915c5e21..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") } @@ -489,3 +491,18 @@ model OutboundEmailSent { @@index([emailCampaignId]) @@map("outbound_emails_sent") } + +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") +} 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/handlers.ts b/packages/app/server/src/handlers.ts index 033649c39..540b5f26e 100644 --- a/packages/app/server/src/handlers.ts +++ b/packages/app/server/src/handlers.ts @@ -23,34 +23,46 @@ import { } from 'services/facilitator/x402-types'; import { Decimal } from '@prisma/client/runtime/library'; import logger from 'logger'; +import { Request, Response } from 'express'; +import { ProviderType } from 'providers/ProviderType'; -export async function handleX402Request({ - req, - res, - headers, - maxCost, - isPassthroughProxyRoute, - provider, - isStream, -}: X402HandlerInput) { - if (isPassthroughProxyRoute) { - return await makeProxyPassthroughRequest(req, res, provider, headers); +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); } +} - // Apply x402 payment middleware with the calculated maxCost +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,100 +74,114 @@ 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 }; +} - const settleResult = await facilitatorClient.settle(settleRequest); +export async function finalize( + paymentAmountDecimal: Decimal, + transaction: Transaction, + payload: ExactEvmPayload +) { + const refundAmount = calculateRefundAmount( + paymentAmountDecimal, + transaction.rawTransactionCost + ); - if (!settleResult.success || !settleResult.transaction) { - return buildX402Response(req, res, maxCost); - } + if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { + const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); + const authPayload = payload.authorization; + await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); + } +} + +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 { payload, paymentAmountDecimal } = settleResult; - 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(), - }); - }); - } + try { + const transactionResult = await modelRequestService.executeModelRequest( + req, + res, + headers, + provider, + isStream + ); + const transaction = transactionResult.transaction; + + + if (provider.getType() === ProviderType.OPENAI_VIDEOS) { + await prisma.videoGenerationX402.create({ + data: { + videoId: transaction.metadata.providerId, + wallet: payload.authorization.from, + cost: transaction.rawTransactionCost, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 1), + }, + }); } + + modelRequestService.handleResolveResponse( + res, + isStream, + transactionResult.data + ); + + await finalize( + paymentAmountDecimal, + transactionResult.transaction, + payload + ); } catch (error) { - logger.error('Error in handleX402Request', { error }); - throw error; + await refund(paymentAmountDecimal, payload); } } @@ -200,10 +226,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: transactionCost.totalTransactionCost, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 1), + }, + }); + } } 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) ); } diff --git a/packages/app/server/src/providers/GroqProvider.ts b/packages/app/server/src/providers/GroqProvider.ts new file mode 100644 index 000000000..b771055c5 --- /dev/null +++ b/packages/app/server/src/providers/GroqProvider.ts @@ -0,0 +1,80 @@ +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 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/OpenAIVideoProvider.ts b/packages/app/server/src/providers/OpenAIVideoProvider.ts index 873fc3caf..155dbf050 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,9 +175,72 @@ export class OpenAIVideoProvider extends BaseProvider { } const responseData = await response.json(); + switch (responseData.status) { + case 'completed': + await this.handleSuccessfulVideoGeneration(responseData.id as string); + break; + case 'failed': + await this.handleFailedVideoGeneration(responseData.id as string); + break; + default: + break; + } res.json(responseData); } + // ====== Refund methods ====== + private async handleSuccessfulVideoGeneration( + videoId: string + ): Promise { + await prisma.$transaction(async tx => { + const result = await tx.$queryRawUnsafe( + `SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`, + videoId + ); + const video = (result as any[])[0]; + if (video && !video.isFinal) { + await tx.videoGenerationX402.update({ + where: { + videoId: video.videoId, + }, + data: { + isFinal: true, + }, + }); + } + }); + } + + private async handleFailedVideoGeneration(videoId: string): Promise { + await prisma.$transaction(async tx => { + 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.isFinal) { + return; + } + if (video.wallet) { + 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, + }, + data: { + isFinal: true, + }, + }); + }); + } + // ========== Video Download Handling ========== private isVideoContentDownload(path: string): boolean { 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/resources/e2b/e2b.ts b/packages/app/server/src/resources/e2b/e2b.ts new file mode 100644 index 000000000..feb1bbb86 --- /dev/null +++ b/packages/app/server/src/resources/e2b/e2b.ts @@ -0,0 +1,73 @@ +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 { 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 + ); +}; + +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'); + } + try { + 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, + }; + } catch (error) { + const errorText = error instanceof Error ? error.message : 'Unknown error'; + throw new HttpError(400, `E2B API request failed: ${errorText}`); + } +}; 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..038e7deb2 --- /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; 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..9fcc3ef51 --- /dev/null +++ b/packages/app/server/src/resources/e2b/route.ts @@ -0,0 +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 { handleResourceRequestWithErrorHandling } from '../handler'; + +export async function e2bExecuteRoute(req: Request, res: Response) { + 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/e2b/types.ts b/packages/app/server/src/resources/e2b/types.ts new file mode 100644 index 000000000..16edb05f2 --- /dev/null +++ b/packages/app/server/src/resources/e2b/types.ts @@ -0,0 +1,82 @@ +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/handler.ts b/packages/app/server/src/resources/handler.ts new file mode 100644 index 000000000..4cfd7107f --- /dev/null +++ b/packages/app/server/src/resources/handler.ts @@ -0,0 +1,166 @@ +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'; +import { HttpError, PaymentRequiredError } from 'errors/http'; + +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, + config: ResourceHandlerConfig +): Promise { + const { executeResource, calculateActualCost, createTransaction } = config; + + const settleResult = await settle(req, res, headers, safeMaxCost); + if (!settleResult) { + throw new PaymentRequiredError('Payment required, settle failed'); + } + + 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)) { + 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 buildX402Response(req, res, safeMaxCost); +} + +export async function handleResourceRequestWithErrorHandling( + req: Request, + res: Response, + config: ResourceHandlerConfig +) { + 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: errorMessage || '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 new file mode 100644 index 000000000..dc7d21732 --- /dev/null +++ b/packages/app/server/src/resources/tavily/crawl/route.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import { TavilyCrawlInputSchema } from './types'; +import { + calculateTavilyCrawlMaxCost, + calculateTavilyCrawlActualCost, + tavilyCrawl, + createTavilyTransaction, +} from './tavily'; +import { handleResourceRequestWithErrorHandling } from '../../handler'; + +export async function tavilyCrawlRoute(req: Request, res: Response) { + 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/crawl/tavily.ts b/packages/app/server/src/resources/tavily/crawl/tavily.ts new file mode 100644 index 000000000..3cbeac46c --- /dev/null +++ b/packages/app/server/src/resources/tavily/crawl/tavily.ts @@ -0,0 +1,113 @@ +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'; +import { HttpError } from 'errors/http'; + +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 HttpError( + response.status, + `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..4d86fb0b6 --- /dev/null +++ b/packages/app/server/src/resources/tavily/crawl/types.ts @@ -0,0 +1,37 @@ +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..c59a37eed --- /dev/null +++ b/packages/app/server/src/resources/tavily/extract/route.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import { TavilyExtractInputSchema } from './types'; +import { + calculateTavilyExtractMaxCost, + calculateTavilyExtractActualCost, + tavilyExtract, + createTavilyTransaction, +} from './tavily'; +import { handleResourceRequestWithErrorHandling } from '../../handler'; + +export async function tavilyExtractRoute(req: Request, res: Response) { + 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 new file mode 100644 index 000000000..f2c2aa8d3 --- /dev/null +++ b/packages/app/server/src/resources/tavily/extract/tavily.ts @@ -0,0 +1,85 @@ +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'; +import { HttpError } from 'errors/http'; + +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 HttpError( + response.status, + `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..5b86549ce --- /dev/null +++ b/packages/app/server/src/resources/tavily/extract/types.ts @@ -0,0 +1,35 @@ +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 new file mode 100644 index 000000000..187f40c73 --- /dev/null +++ b/packages/app/server/src/resources/tavily/prices.ts @@ -0,0 +1,31 @@ +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; diff --git a/packages/app/server/src/resources/tavily/search/route.ts b/packages/app/server/src/resources/tavily/search/route.ts new file mode 100644 index 000000000..12ba059a5 --- /dev/null +++ b/packages/app/server/src/resources/tavily/search/route.ts @@ -0,0 +1,19 @@ +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) { + return handleResourceRequestWithErrorHandling(req, res, { + inputSchema: TavilySearchInputSchema, + calculateMaxCost: input => calculateTavilySearchCost(input), + executeResource: tavilySearch, + calculateActualCost: input => calculateTavilySearchCost(input), + createTransaction: createTavilyTransaction, + errorMessage: 'Error searching tavily', + }); +} diff --git a/packages/app/server/src/resources/tavily/search/tavily.ts b/packages/app/server/src/resources/tavily/search/tavily.ts new file mode 100644 index 000000000..8ff82f6e2 --- /dev/null +++ b/packages/app/server/src/resources/tavily/search/tavily.ts @@ -0,0 +1,60 @@ +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 { HttpError } from 'errors/http'; + +export const calculateTavilySearchCost = ( + input: TavilySearchInput | undefined +): 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 +): 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 HttpError( + response.status, + `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/search/types.ts b/packages/app/server/src/resources/tavily/search/types.ts new file mode 100644 index 000000000..33f7107ba --- /dev/null +++ b/packages/app/server/src/resources/tavily/search/types.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; + +// Input schema +export const TavilySearchInputSchema = z.object({ + query: z.string(), + auto_parameters: z.coerce.boolean().optional(), + topic: z.enum(['general', 'news']).optional(), + search_depth: z.enum(['basic', 'advanced']).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.coerce.number().int().positive().optional(), + start_date: z.string().optional(), + end_date: z.string().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(), +}); + +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().nullish(), + 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 new file mode 100644 index 000000000..48fdb05d6 --- /dev/null +++ b/packages/app/server/src/routers/resource.ts @@ -0,0 +1,25 @@ +import { Request, Response, Router } from 'express'; +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(); + +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); +}); + +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 8ad4253c8..fa44b51b9 100644 --- a/packages/app/server/src/schema/schemaForRoute.ts +++ b/packages/app/server/src/schema/schemaForRoute.ts @@ -9,10 +9,24 @@ import { import { z } from 'zod'; import { ChatCompletionInput, ChatCompletionOutput } from './chat/completions'; import { CreateImagesRequest, CreateImagesResponse } from './image/openai'; +import { + TavilySearchInputSchema, + TavilySearchOutputSchema, +} from 'resources/tavily/search/types'; +import { + TavilyExtractInputSchema, + TavilyExtractOutputSchema, +} from 'resources/tavily/extract/types'; +import { + TavilyCrawlInputSchema, + TavilyCrawlOutputSchema, +} from 'resources/tavily/crawl/types'; +import { + E2BExecuteInputSchema, + E2BExecuteOutputSchema, +} from 'resources/e2b/types'; -export function getSchemaForRoute( - path: string -): +export function getSchemaForRoute(path: string): | { input: { type: 'http'; method: string; bodyFields?: unknown }; output: unknown; @@ -82,5 +96,70 @@ 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', + }); + return { + input: { + type: 'http', + method: 'POST', + bodyFields: inputSchema.properties, + }, + 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', + }); + 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/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/server.ts b/packages/app/server/src/server.ts index c194d7929..6d3a8724d 100644 --- a/packages/app/server/src/server.ts +++ b/packages/app/server/src/server.ts @@ -30,6 +30,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(); @@ -94,6 +95,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 { diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index ba654064a..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 @@ -124,10 +126,22 @@ 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 = ( 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 (
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/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 34f82e35a..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,9 +20,9 @@ import { handleEchoClientProxy } from 'proxy'; import { handleCallback, handleRefresh, + handleSession, handleSignIn, handleSignOut, - handleSession, } from './auth/oauth-handlers'; /** @@ -112,5 +114,7 @@ export default function Echo(config: EchoConfig): EchoResult { openai: createEchoOpenAI(config), 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/package.json b/packages/sdk/react/package.json index de0618dfb..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.35", + "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/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/sdk/ts/package.json b/packages/sdk/ts/package.json index ed8f5d1cf..93ea1657a 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.22", "description": "TypeScript SDK for Echo platform", "type": "module", "main": "./dist/index.cjs", @@ -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", @@ -57,6 +59,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/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/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/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/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'; diff --git a/packages/sdk/ts/src/supported-models/chat/anthropic.ts b/packages/sdk/ts/src/supported-models/chat/anthropic.ts index 6dfdb650e..de039a1fc 100644 --- a/packages/sdk/ts/src/supported-models/chat/anthropic.ts +++ b/packages/sdk/ts/src/supported-models/chat/anthropic.ts @@ -8,9 +8,11 @@ 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'; + | 'claude-sonnet-4-20250514' + | 'claude-sonnet-4-5-20250929'; export const AnthropicModels: SupportedModel[] = [ { @@ -49,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, @@ -67,4 +75,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/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/groq.ts b/packages/sdk/ts/src/supported-models/chat/groq.ts new file mode 100644 index 000000000..7447e8246 --- /dev/null +++ b/packages/sdk/ts/src/supported-models/chat/groq.ts @@ -0,0 +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 = + | 'llama-3.1-8b-instant' + | 'llama-3.3-70b-versatile' + | '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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/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 d7321a2f8..0c07dbd2d 100644 --- a/packages/sdk/ts/src/supported-models/chat/openrouter.ts +++ b/packages/sdk/ts/src/supported-models/chat/openrouter.ts @@ -2,136 +2,108 @@ 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' + | 'deepcogito/cogito-v2-preview-llama-405b' + | 'deepcogito/cogito-v2-preview-llama-70b' | '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' + | 'inclusionai/ling-1t' + | 'inclusionai/ring-1t' | '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' @@ -139,14 +111,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' @@ -154,50 +124,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' @@ -216,7 +177,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' @@ -224,118 +184,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' - | 'openrouter/auto' - | '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 +309,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 +355,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 +379,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 +403,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, @@ -474,6 +427,18 @@ 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/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, @@ -486,12 +451,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, @@ -499,19 +458,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', @@ -540,112 +493,70 @@ 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, + input_cost_per_token: 0.0000025, + output_cost_per_token: 0.00001, provider: 'OpenRouter', }, { - model_id: 'cohere/command-r', - input_cost_per_token: 5e-7, - output_cost_per_token: 0.0000015, + model_id: 'cohere/command-r-08-2024', + input_cost_per_token: 1.5e-7, + output_cost_per_token: 6e-7, provider: 'OpenRouter', }, { - model_id: 'cohere/command-r-03-2024', - input_cost_per_token: 5e-7, - output_cost_per_token: 0.0000015, + model_id: 'cohere/command-r-plus-08-2024', + input_cost_per_token: 0.0000025, + output_cost_per_token: 0.00001, provider: 'OpenRouter', }, { - model_id: 'cohere/command-r-08-2024', - input_cost_per_token: 1.5e-7, - output_cost_per_token: 6e-7, + model_id: 'cohere/command-r7b-12-2024', + input_cost_per_token: 3.75e-8, + output_cost_per_token: 1.5e-7, provider: 'OpenRouter', }, { - model_id: 'cohere/command-r-plus', - input_cost_per_token: 0.000003, - output_cost_per_token: 0.000015, + model_id: 'deepcogito/cogito-v2-preview-deepseek-671b', + input_cost_per_token: 0.00000125, + output_cost_per_token: 0.00000125, provider: 'OpenRouter', }, { - model_id: 'cohere/command-r-plus-04-2024', - input_cost_per_token: 0.000003, - output_cost_per_token: 0.000015, + 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: 'cohere/command-r-plus-08-2024', - input_cost_per_token: 0.0000025, - output_cost_per_token: 0.00001, + model_id: 'deepcogito/cogito-v2-preview-llama-405b', + input_cost_per_token: 0.0000035, + output_cost_per_token: 0.0000035, provider: 'OpenRouter', }, { - model_id: 'cohere/command-r7b-12-2024', - input_cost_per_token: 3.75e-8, - output_cost_per_token: 1.5e-7, + 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: 1.999188e-7, - output_cost_per_token: 8.00064e-7, + 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: 1.999188e-7, - output_cost_per_token: 8.00064e-7, - provider: 'OpenRouter', - }, - { - model_id: 'deepseek/deepseek-chat-v3-0324:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + input_cost_per_token: 2.4e-7, + output_cost_per_token: 8.4e-7, provider: 'OpenRouter', }, { @@ -668,50 +579,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', }, { @@ -720,28 +601,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', }, { @@ -756,12 +631,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, @@ -787,45 +656,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', }, { @@ -837,55 +694,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', }, { @@ -894,16 +721,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', }, { @@ -919,9 +740,15 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'infermatic/mn-inferor-12b', - input_cost_per_token: 6e-7, - output_cost_per_token: 0.000001, + 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', }, { @@ -954,6 +781,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, @@ -968,8 +801,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', }, { @@ -978,22 +811,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', }, { @@ -1002,12 +829,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, @@ -1016,38 +837,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', }, { @@ -1056,24 +859,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, @@ -1094,14 +885,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', }, { @@ -1146,16 +931,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', }, { @@ -1184,14 +963,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', }, { @@ -1237,15 +1010,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', }, { @@ -1280,14 +1053,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', }, { @@ -1304,38 +1071,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', }, { @@ -1352,8 +1101,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', }, { @@ -1369,9 +1118,9 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'moonshotai/kimi-dev-72b: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', }, { @@ -1381,27 +1130,15 @@ export const OpenRouterModels: SupportedModel[] = [ provider: 'OpenRouter', }, { - model_id: 'moonshotai/kimi-k2:free', - input_cost_per_token: 0, - output_cost_per_token: 0, - provider: 'OpenRouter', - }, - { - model_id: 'moonshotai/kimi-vl-a3b-thinking', - input_cost_per_token: 2.498985e-8, - output_cost_per_token: 1.00008e-7, - 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', }, { @@ -1410,12 +1147,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, @@ -1429,59 +1160,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, @@ -1489,15 +1208,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', }, { @@ -1608,12 +1327,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, @@ -1656,6 +1369,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, @@ -1669,21 +1388,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', }, { @@ -1716,6 +1435,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, @@ -1741,27 +1466,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, - provider: 'OpenRouter', - }, - { - model_id: 'opengvlab/internvl3-14b', - input_cost_per_token: 2e-7, - output_cost_per_token: 4e-7, + model_id: 'openai/o4-mini-deep-research', + input_cost_per_token: 0.000002, + output_cost_per_token: 0.000008, provider: 'OpenRouter', }, { - model_id: 'openrouter/auto', - input_cost_per_token: -1, - output_cost_per_token: -1, + 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 +1513,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 +1525,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 +1543,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 +1574,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/qwen3-vl-235b-a22b-thinking', + 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-30b-a3b-instruct', + input_cost_per_token: 2.9e-7, + output_cost_per_token: 9.9e-7, 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-thinking', + input_cost_per_token: 2.9e-7, + output_cost_per_token: 0.000001, provider: 'OpenRouter', }, { - model_id: 'qwen/qwq-32b:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + 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: 'raifle/sorcererlm-8x22b', - input_cost_per_token: 0.0000045, - output_cost_per_token: 0.0000045, + model_id: 'qwen/qwen3-vl-8b-thinking', + input_cost_per_token: 1.8e-7, + output_cost_per_token: 0.0000021, provider: 'OpenRouter', }, { - model_id: 'rekaai/reka-flash-3:free', - input_cost_per_token: 0, - output_cost_per_token: 0, + model_id: 'qwen/qwq-32b', + input_cost_per_token: 1.5e-7, + output_cost_per_token: 4e-7, + provider: 'OpenRouter', + }, + { + model_id: 'raifle/sorcererlm-8x22b', + input_cost_per_token: 0.0000045, + output_cost_per_token: 0.0000045, provider: 'OpenRouter', }, { @@ -2024,10 +1737,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 +1759,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 +1783,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 +1803,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 +1813,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 +1821,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 +1843,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 +1874,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 +1893,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/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); 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 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/groq-generate-text.test.ts b/packages/tests/provider-smoke/groq-generate-text.test.ts new file mode 100644 index 000000000..4e6e0cf46 --- /dev/null +++ b/packages/tests/provider-smoke/groq-generate-text.test.ts @@ -0,0 +1,36 @@ +import { createEchoGroq, GroqModels } 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 GroqModels) { + 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..05e663c17 --- /dev/null +++ b/packages/tests/provider-smoke/groq-stream-text.test.ts @@ -0,0 +1,42 @@ +import { createEchoGroq, GroqModels } 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 = [ + '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 GroqModels) { + 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/packages/tests/provider-smoke/openai-generate-text.test.ts b/packages/tests/provider-smoke/openai-generate-text.test.ts index 784e5d4c1..2cffc16eb 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); 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..b12ef0d04 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,7 +22,11 @@ describe.concurrent('OpenRouter generateText per model', () => { ); for (const { model_id } of OpenRouterModels) { - it(`OpenAI ${model_id}`, async () => { + if (shouldSkipModelInTests(model_id)) { + continue; + } + + it(`OpenRouter generateText ${model_id}`, async () => { try { const { text } = await generateText({ model: openrouter(model_id), @@ -35,6 +40,6 @@ describe.concurrent('OpenRouter generateText per model', () => { `[generateText] OpenRouter ${model_id} failed: ${details}` ); } - }); + }); // 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..72d969d1d 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}` ); } - }); + }); // 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; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 013382483..bd9d05738 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,9 @@ importers: '@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)) + '@e2b/code-interpreter': + specifier: ^2.0.1 + version: 2.0.1 '@google-cloud/storage': specifier: ^7.17.1 version: 7.17.1 @@ -1203,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) @@ -1231,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) @@ -1416,6 +1425,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'} @@ -1428,6 +1443,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'} @@ -1687,6 +1708,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 +1754,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 +1807,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 +7014,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 +7077,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 +7939,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'} @@ -7943,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'} @@ -8458,6 +8512,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 +9611,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 +9746,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 +9842,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'} @@ -11950,6 +12018,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 @@ -11964,6 +12038,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 @@ -12427,6 +12508,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 @@ -12659,6 +12742,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 +12795,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 @@ -19047,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: @@ -21582,6 +21678,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 +21750,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 +22936,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: {} @@ -22871,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 @@ -23470,6 +23605,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 @@ -24923,6 +25062,10 @@ snapshots: 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 +25319,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 +25413,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 +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 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();