diff --git a/.cursor/skills/add-hosted-key/SKILL.md b/.cursor/skills/add-hosted-key/SKILL.md new file mode 100644 index 00000000000..2a5888c5f11 --- /dev/null +++ b/.cursor/skills/add-hosted-key/SKILL.md @@ -0,0 +1,257 @@ +--- +name: add-hosted-key +description: Add hosted API key support to a tool so Sim provides the key when users don't bring their own. Use when adding hosted keys, BYOK support, hideWhenHosted, or hosted key pricing to a tool or block. +--- + +# Adding Hosted Key Support to a Tool + +When a tool has hosted key support, Sim provides its own API key if the user hasn't configured one (via BYOK or env var). Usage is metered and billed to the workspace. + +## Overview + +| Step | What | Where | +|------|------|-------| +| 1 | Register BYOK provider ID | `tools/types.ts`, `app/api/workspaces/[id]/byok-keys/route.ts` | +| 2 | Research the API's pricing and rate limits | API docs / pricing page (before writing any code) | +| 3 | Add `hosting` config to the tool | `tools/{service}/{action}.ts` | +| 4 | Hide API key field when hosted | `blocks/blocks/{service}.ts` | +| 5 | Add to BYOK settings UI | BYOK settings component (`byok.tsx`) | +| 6 | Summarize pricing and throttling comparison | Output to user (after all code changes) | + +## Step 1: Register the BYOK Provider ID + +Add the new provider to the `BYOKProviderId` union in `tools/types.ts`: + +```typescript +export type BYOKProviderId = + | 'openai' + | 'anthropic' + // ...existing providers + | 'your_service' +``` + +Then add it to `VALID_PROVIDERS` in `app/api/workspaces/[id]/byok-keys/route.ts`: + +```typescript +const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'your_service'] as const +``` + +## Step 2: Research the API's Pricing Model and Rate Limits + +**Before writing any `getCost` or `rateLimit` code**, look up the service's official documentation for both pricing and rate limits. You need to understand: + +### Pricing + +1. **How the API charges** — per request, per credit, per token, per step, per minute, etc. +2. **Whether the API reports cost in its response** — look for fields like `creditsUsed`, `costDollars`, `tokensUsed`, or similar in the response body or headers +3. **Whether cost varies by endpoint/options** — some APIs charge more for certain features (e.g., Firecrawl charges 1 credit/page base but +4 for JSON format, +4 for enhanced mode) +4. **The dollar-per-unit rate** — what each credit/token/unit costs in dollars on our plan + +### Rate Limits + +1. **What rate limits the API enforces** — requests per minute/second, tokens per minute, concurrent requests, etc. +2. **Whether limits vary by plan tier** — free vs paid vs enterprise often have different ceilings +3. **Whether limits are per-key or per-account** — determines whether adding more hosted keys actually increases total throughput +4. **What the API returns when rate limited** — HTTP 429, `Retry-After` header, error body format, etc. +5. **Whether there are multiple dimensions** — some APIs limit both requests/min AND tokens/min independently + +Search the API's docs/pricing page (use WebSearch/WebFetch). Capture the pricing model as a comment in `getCost` so future maintainers know the source of truth. + +### Setting Our Rate Limits + +Our rate limiter (`lib/core/rate-limiter/hosted-key/`) uses a token-bucket algorithm applied **per billing actor** (workspace). It supports two modes: + +- **`per_request`** — simple; just `requestsPerMinute`. Good when the API charges flat per-request or cost doesn't vary much. +- **`custom`** — `requestsPerMinute` plus additional `dimensions` (e.g., `tokens`, `search_units`). Each dimension has its own `limitPerMinute` and an `extractUsage` function that reads actual usage from the response. Use when the API charges on a variable metric (tokens, credits) and you want to cap that metric too. + +When choosing values for `requestsPerMinute` and any dimension limits: + +- **Stay well below the API's per-key limit** — our keys are shared across all workspaces. If the API allows 60 RPM per key and we have 3 keys, the global ceiling is ~180 RPM. Set the per-workspace limit low enough (e.g., 20-60 RPM) that many workspaces can coexist without collectively hitting the API's ceiling. +- **Account for key pooling** — our round-robin distributes requests across `N` hosted keys, so the effective API-side rate per key is `(total requests) / N`. But per-workspace limits are enforced *before* key selection, so they apply regardless of key count. +- **Prefer conservative defaults** — it's easy to raise limits later but hard to claw back after users depend on high throughput. + +## Step 3: Add `hosting` Config to the Tool + +Add a `hosting` object to the tool's `ToolConfig`. This tells the execution layer how to acquire hosted keys, calculate cost, and rate-limit. + +```typescript +hosting: { + envKeyPrefix: 'YOUR_SERVICE_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'your_service', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 // dollars per credit + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, +}, +``` + +### Hosted Key Env Var Convention + +Keys use a numbered naming pattern driven by a count env var: + +``` +YOUR_SERVICE_API_KEY_COUNT=3 +YOUR_SERVICE_API_KEY_1=sk-... +YOUR_SERVICE_API_KEY_2=sk-... +YOUR_SERVICE_API_KEY_3=sk-... +``` + +The `envKeyPrefix` value (`YOUR_SERVICE_API_KEY`) determines which env vars are read at runtime. Adding more keys only requires bumping the count and adding the new env var. + +### Pricing: Prefer API-Reported Cost + +Always prefer using cost data returned by the API (e.g., `creditsUsed`, `costDollars`). This is the most accurate because it accounts for variable pricing tiers, feature modifiers, and plan-level discounts. + +**When the API reports cost** — use it directly and throw if missing: + +```typescript +pricing: { + type: 'custom', + getCost: (params, output) => { + if (output.creditsUsed == null) { + throw new Error('Response missing creditsUsed field') + } + // $0.001 per credit — from https://example.com/pricing + const cost = (output.creditsUsed as number) * 0.001 + return { cost, metadata: { creditsUsed: output.creditsUsed } } + }, +}, +``` + +**When the API does NOT report cost** — compute it from params/output based on the pricing docs, but still validate the data you depend on: + +```typescript +pricing: { + type: 'custom', + getCost: (params, output) => { + if (!Array.isArray(output.searchResults)) { + throw new Error('Response missing searchResults, cannot determine cost') + } + // Serper: 1 credit for <=10 results, 2 credits for >10 — from https://serper.dev/pricing + const credits = Number(params.num) > 10 ? 2 : 1 + return { cost: credits * 0.001, metadata: { credits } } + }, +}, +``` + +**`getCost` must always throw** if it cannot determine cost. Never silently fall back to a default — this would hide billing inaccuracies. + +### Capturing Cost Data from the API + +If the API returns cost info, capture it in `transformResponse` so `getCost` can read it from the output: + +```typescript +transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + results: data.results, + creditsUsed: data.creditsUsed, // pass through for getCost + }, + } +}, +``` + +For async/polling tools, capture it in `postProcess` when the job completes: + +```typescript +if (jobData.status === 'completed') { + result.output = { + data: jobData.data, + creditsUsed: jobData.creditsUsed, + } +} +``` + +## Step 4: Hide the API Key Field When Hosted + +In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` to the API key subblock. This hides the field on hosted Sim since the platform provides the key: + +```typescript +{ + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your API key', + password: true, + required: true, + hideWhenHosted: true, +}, +``` + +The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag. + +## Step 5: Add to the BYOK Settings UI + +Add an entry to the `PROVIDERS` array in the BYOK settings component so users can bring their own key. You need the service icon from `components/icons.tsx`: + +```typescript +{ + id: 'your_service', + name: 'Your Service', + icon: YourServiceIcon, + description: 'What this service does', + placeholder: 'Enter your API key', +}, +``` + +## Step 6: Summarize Pricing and Throttling Comparison + +After all code changes are complete, output a detailed summary to the user covering: + +### What to include + +1. **API's pricing model** — how the service charges (per token, per credit, per request, etc.), the specific rates found in docs, and whether the API reports cost in responses. +2. **Our `getCost` approach** — how we calculate cost, what fields we depend on, and any assumptions or estimates (especially when the API doesn't report exact dollar cost). +3. **API's rate limits** — the documented limits (RPM, TPM, concurrent, etc.), which plan tier they apply to, and whether they're per-key or per-account. +4. **Our `rateLimit` config** — what we set for `requestsPerMinute` (and dimensions if custom mode), why we chose those values, and how they compare to the API's limits. +5. **Key pooling impact** — how many hosted keys we expect, and how round-robin distribution affects the effective per-key rate at the API. +6. **Gaps or risks** — anything the API charges for that we don't meter, rate limit dimensions we chose not to enforce, or pricing that may be inaccurate due to variable model/tier costs. + +### Format + +Present this as a structured summary with clear headings. Example: + +``` +### Pricing +- **API charges**: $X per 1M tokens (input), $Y per 1M tokens (output) — varies by model +- **Response reports cost?**: No — only token counts in `usage` field +- **Our getCost**: Estimates cost at $Z per 1M total tokens based on median model pricing +- **Risk**: Actual cost varies by model; our estimate may over/undercharge for cheap/expensive models + +### Throttling +- **API limits**: 300 RPM per key (paid tier), 60 RPM (free tier) +- **Per-key or per-account**: Per key — more keys = more throughput +- **Our config**: 60 RPM per workspace (per_request mode) +- **With N keys**: Effective per-key rate is (total RPM across workspaces) / N +- **Headroom**: Comfortable — even 10 active workspaces at full rate = 600 RPM / 3 keys = 200 RPM per key, under the 300 RPM API limit +``` + +This summary helps reviewers verify that the pricing and rate limiting are well-calibrated and surfaces any risks that need monitoring. + +## Checklist + +- [ ] Provider added to `BYOKProviderId` in `tools/types.ts` +- [ ] Provider added to `VALID_PROVIDERS` in the BYOK keys API route +- [ ] API pricing docs researched — understand per-unit cost and whether the API reports cost in responses +- [ ] API rate limits researched — understand RPM/TPM limits, per-key vs per-account, and plan tiers +- [ ] `hosting` config added to the tool with `envKeyPrefix`, `apiKeyParam`, `byokProviderId`, `pricing`, and `rateLimit` +- [ ] `getCost` throws if required cost data is missing from the response +- [ ] Cost data captured in `transformResponse` or `postProcess` if API provides it +- [ ] `hideWhenHosted: true` added to the API key subblock in the block config +- [ ] Provider entry added to the BYOK settings UI with icon and description +- [ ] Env vars documented: `{PREFIX}_COUNT` and `{PREFIX}_1..N` +- [ ] Pricing and throttling summary provided to reviewer diff --git a/apps/sim/app/api/files/utils.ts b/apps/sim/app/api/files/utils.ts index 953c9b8989c..1989ab1d3d6 100644 --- a/apps/sim/app/api/files/utils.ts +++ b/apps/sim/app/api/files/utils.ts @@ -1,5 +1,5 @@ import { existsSync } from 'fs' -import { join, resolve, sep } from 'path' +import path from 'path' import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { UPLOAD_DIR } from '@/lib/uploads/config' @@ -155,7 +155,7 @@ function sanitizeFilename(filename: string): string { return sanitized }) - return sanitizedSegments.join(sep) + return sanitizedSegments.join(path.sep) } export function findLocalFile(filename: string): string | null { @@ -168,17 +168,18 @@ export function findLocalFile(filename: string): string | null { } const possiblePaths = [ - join(UPLOAD_DIR, sanitizedFilename), - join(process.cwd(), 'uploads', sanitizedFilename), + path.join(UPLOAD_DIR, sanitizedFilename), + path.join(process.cwd(), 'uploads', sanitizedFilename), ] - for (const path of possiblePaths) { - const resolvedPath = resolve(path) - const allowedDirs = [resolve(UPLOAD_DIR), resolve(process.cwd(), 'uploads')] + for (const filePath of possiblePaths) { + const resolvedPath = path.resolve(filePath) + const allowedDirs = [path.resolve(UPLOAD_DIR), path.resolve(process.cwd(), 'uploads')] // Must be within allowed directory but NOT the directory itself const isWithinAllowedDir = allowedDirs.some( - (allowedDir) => resolvedPath.startsWith(allowedDir + sep) && resolvedPath !== allowedDir + (allowedDir) => + resolvedPath.startsWith(allowedDir + path.sep) && resolvedPath !== allowedDir ) if (!isWithinAllowedDir) { diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts index f4bddc4298b..ff5cdab3dd7 100644 --- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts @@ -13,7 +13,23 @@ import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/per const logger = createLogger('WorkspaceBYOKKeysAPI') -const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'exa'] as const +const VALID_PROVIDERS = [ + 'openai', + 'anthropic', + 'google', + 'mistral', + 'exa', + 'browser_use', + 'serper', + 'firecrawl', + 'linkup', + 'perplexity', + 'jina', + 'google_cloud', + 'elevenlabs', + 'parallel_ai', + 'brandfetch', +] as const const UpsertKeySchema = z.object({ providerId: z.enum(VALID_PROVIDERS), diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index b956bcecac0..63e90c43b36 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -13,7 +13,23 @@ import { ModalFooter, ModalHeader, } from '@/components/emcn' -import { AnthropicIcon, ExaAIIcon, GeminiIcon, MistralIcon, OpenAIIcon } from '@/components/icons' +import { + AnthropicIcon, + BrandfetchIcon, + BrowserUseIcon, + ElevenLabsIcon, + ExaAIIcon, + FirecrawlIcon, + GeminiIcon, + GoogleIcon, + JinaAIIcon, + LinkupIcon, + MistralIcon, + OpenAIIcon, + ParallelIcon, + PerplexityIcon, + SerperIcon, +} from '@/components/icons' import { Skeleton } from '@/components/ui' import { type BYOKKey, @@ -67,6 +83,76 @@ const PROVIDERS: { description: 'AI-powered search and research', placeholder: 'Enter your Exa API key', }, + { + id: 'browser_use', + name: 'Browser Use', + icon: BrowserUseIcon, + description: 'Browser automation tasks', + placeholder: 'Enter your Browser Use API key', + }, + { + id: 'serper', + name: 'Serper', + icon: SerperIcon, + description: 'Google search API', + placeholder: 'Enter your Serper API key', + }, + { + id: 'firecrawl', + name: 'Firecrawl', + icon: FirecrawlIcon, + description: 'Web scraping, crawling, and data extraction', + placeholder: 'Enter your Firecrawl API key', + }, + { + id: 'linkup', + name: 'Linkup', + icon: LinkupIcon, + description: 'Web search and content retrieval', + placeholder: 'Enter your Linkup API key', + }, + { + id: 'parallel_ai', + name: 'Parallel AI', + icon: ParallelIcon, + description: 'Web search, extraction, and deep research', + placeholder: 'Enter your Parallel AI API key', + }, + { + id: 'perplexity', + name: 'Perplexity', + icon: PerplexityIcon, + description: 'AI-powered chat and web search', + placeholder: 'pplx-...', + }, + { + id: 'jina', + name: 'Jina AI', + icon: JinaAIIcon, + description: 'Web reading and search', + placeholder: 'jina_...', + }, + { + id: 'elevenlabs', + name: 'ElevenLabs', + icon: ElevenLabsIcon, + description: 'Text-to-speech generation', + placeholder: 'Enter your ElevenLabs API key', + }, + { + id: 'google_cloud', + name: 'Google Cloud', + icon: GoogleIcon, + description: 'Translate, Maps, PageSpeed, and Books APIs', + placeholder: 'Enter your Google Cloud API key', + }, + { + id: 'brandfetch', + name: 'Brandfetch', + icon: BrandfetchIcon, + description: 'Brand assets, logos, colors, and company info', + placeholder: 'Enter your Brandfetch API key', + }, ] function BYOKKeySkeleton() { diff --git a/apps/sim/blocks/blocks/brandfetch.ts b/apps/sim/blocks/blocks/brandfetch.ts index a270ce45fe9..68bbcd1db30 100644 --- a/apps/sim/blocks/blocks/brandfetch.ts +++ b/apps/sim/blocks/blocks/brandfetch.ts @@ -49,6 +49,7 @@ export const BrandfetchBlock: BlockConfig = { password: true, placeholder: 'Enter your BrowserUse API key', required: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/blocks/blocks/elevenlabs.ts b/apps/sim/blocks/blocks/elevenlabs.ts index 58d79fe67ae..ee41007f674 100644 --- a/apps/sim/blocks/blocks/elevenlabs.ts +++ b/apps/sim/blocks/blocks/elevenlabs.ts @@ -48,6 +48,7 @@ export const ElevenLabsBlock: BlockConfig = { placeholder: 'Enter your ElevenLabs API key', password: true, required: true, + hideWhenHosted: true, }, ], diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index 2fbddd14988..d1537b0c2e6 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -248,6 +248,7 @@ Example 2 - Product Data: placeholder: 'Enter your Firecrawl API key', password: true, required: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/blocks/blocks/google_books.ts b/apps/sim/blocks/blocks/google_books.ts index 764ee0290c7..1b36cd7e4ae 100644 --- a/apps/sim/blocks/blocks/google_books.ts +++ b/apps/sim/blocks/blocks/google_books.ts @@ -32,6 +32,7 @@ export const GoogleBooksBlock: BlockConfig = { password: true, placeholder: 'Enter your Google Books API key', required: true, + hideWhenHosted: true, }, { id: 'query', diff --git a/apps/sim/blocks/blocks/google_maps.ts b/apps/sim/blocks/blocks/google_maps.ts index 06ee7879c85..53a0fcf25ae 100644 --- a/apps/sim/blocks/blocks/google_maps.ts +++ b/apps/sim/blocks/blocks/google_maps.ts @@ -44,6 +44,7 @@ export const GoogleMapsBlock: BlockConfig = { password: true, placeholder: 'Enter your Google Maps API key', required: true, + hideWhenHosted: true, }, // ========== Geocode ========== diff --git a/apps/sim/blocks/blocks/google_pagespeed.ts b/apps/sim/blocks/blocks/google_pagespeed.ts index 955b895cbd0..eca3a30996e 100644 --- a/apps/sim/blocks/blocks/google_pagespeed.ts +++ b/apps/sim/blocks/blocks/google_pagespeed.ts @@ -58,6 +58,7 @@ export const GooglePagespeedBlock: BlockConfig = required: true, placeholder: 'Enter your Google PageSpeed API key', password: true, + hideWhenHosted: true, }, ], diff --git a/apps/sim/blocks/blocks/google_translate.ts b/apps/sim/blocks/blocks/google_translate.ts index 19c3236fb60..2eaeb2bb22f 100644 --- a/apps/sim/blocks/blocks/google_translate.ts +++ b/apps/sim/blocks/blocks/google_translate.ts @@ -192,6 +192,7 @@ export const GoogleTranslateBlock: BlockConfig = { placeholder: 'Enter your Google Cloud API key', password: true, required: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/blocks/blocks/jina.ts b/apps/sim/blocks/blocks/jina.ts index 27f50cc1cfc..4f9be7884c9 100644 --- a/apps/sim/blocks/blocks/jina.ts +++ b/apps/sim/blocks/blocks/jina.ts @@ -144,6 +144,7 @@ export const JinaBlock: BlockConfig = { required: true, placeholder: 'Enter your Jina API key', password: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/blocks/blocks/linkup.ts b/apps/sim/blocks/blocks/linkup.ts index 31474492219..c9cfc4953e4 100644 --- a/apps/sim/blocks/blocks/linkup.ts +++ b/apps/sim/blocks/blocks/linkup.ts @@ -120,6 +120,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n placeholder: 'Enter your Linkup API key', password: true, required: true, + hideWhenHosted: true, }, ], diff --git a/apps/sim/blocks/blocks/mistral_parse.ts b/apps/sim/blocks/blocks/mistral_parse.ts index dca0faf33da..01f6fdc4fa1 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -327,6 +327,7 @@ export const MistralParseV3Block: BlockConfig = { placeholder: 'Enter your Mistral API key', password: true, required: true, + hideWhenHosted: true, }, ], tools: { @@ -334,15 +335,14 @@ export const MistralParseV3Block: BlockConfig = { config: { tool: () => 'mistral_parser_v3', params: (params) => { - if (!params || !params.apiKey || params.apiKey.trim() === '') { - throw new Error('Mistral API key is required') - } - const parameters: Record = { - apiKey: params.apiKey.trim(), resultType: params.resultType || 'markdown', } + if (params.apiKey?.trim()) { + parameters.apiKey = params.apiKey.trim() + } + // V3 pattern: use canonical document param directly const documentInput = normalizeFileInput(params.document, { single: true }) if (!documentInput) { diff --git a/apps/sim/blocks/blocks/parallel.ts b/apps/sim/blocks/blocks/parallel.ts index 96453d37b66..00ce09609d7 100644 --- a/apps/sim/blocks/blocks/parallel.ts +++ b/apps/sim/blocks/blocks/parallel.ts @@ -143,6 +143,7 @@ export const ParallelBlock: BlockConfig = { placeholder: 'Enter your Parallel AI API key', password: true, required: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/blocks/blocks/perplexity.ts b/apps/sim/blocks/blocks/perplexity.ts index 87a48cf45dd..eb88fe81b4c 100644 --- a/apps/sim/blocks/blocks/perplexity.ts +++ b/apps/sim/blocks/blocks/perplexity.ts @@ -171,6 +171,7 @@ Return ONLY the date string in MM/DD/YYYY format - no explanations, no quotes, n placeholder: 'Enter your Perplexity API key', password: true, required: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/blocks/blocks/serper.ts b/apps/sim/blocks/blocks/serper.ts index ed4eb2e6fde..202de8ef70b 100644 --- a/apps/sim/blocks/blocks/serper.ts +++ b/apps/sim/blocks/blocks/serper.ts @@ -78,6 +78,7 @@ export const SerperBlock: BlockConfig = { placeholder: 'Enter your Serper API key', password: true, required: true, + hideWhenHosted: true, }, ], tools: { diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 2ae7a87afb1..6c7b0369f7b 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -21,9 +21,9 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' +export const isHosted = true + // getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || + // getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled diff --git a/apps/sim/lib/core/telemetry.ts b/apps/sim/lib/core/telemetry.ts index 8d1a08279db..5bb0c6227b7 100644 --- a/apps/sim/lib/core/telemetry.ts +++ b/apps/sim/lib/core/telemetry.ts @@ -938,7 +938,7 @@ export const PlatformEvents = { * Track when a rate limit error is surfaced to the end user (not retried/absorbed). * Fires for both billing-actor limits and exhausted upstream retries. */ - userThrottled: (attrs: { + hostedKeyUserThrottled: (attrs: { toolId: string reason: 'billing_actor_limit' | 'upstream_retries_exhausted' provider?: string @@ -947,7 +947,7 @@ export const PlatformEvents = { workspaceId?: string workflowId?: string }) => { - trackPlatformEvent('platform.user.throttled', { + trackPlatformEvent('platform.hosted_key.user_throttled', { 'tool.id': attrs.toolId, 'throttle.reason': attrs.reason, ...(attrs.provider && { 'provider.id': attrs.provider }), @@ -983,6 +983,18 @@ export const PlatformEvents = { }) }, + hostedKeyUnknownModelCost: (attrs: { + toolId: string + modelName: string + defaultCost: number + }) => { + trackPlatformEvent('platform.hosted_key.unknown_model_cost', { + 'tool.id': attrs.toolId, + 'model.name': attrs.modelName, + 'cost.default_cost': attrs.defaultCost, + }) + }, + /** * Track chat deployed (workflow deployed as chat interface) */ diff --git a/apps/sim/tools/brandfetch/get_brand.ts b/apps/sim/tools/brandfetch/get_brand.ts index 77affe1b8b9..c215f6619d6 100644 --- a/apps/sim/tools/brandfetch/get_brand.ts +++ b/apps/sim/tools/brandfetch/get_brand.ts @@ -11,6 +11,21 @@ export const brandfetchGetBrandTool: ToolConfig< 'Retrieve brand assets including logos, colors, fonts, and company info by domain, ticker, ISIN, or crypto symbol', version: '1.0.0', + hosting: { + envKeyPrefix: 'BRANDFETCH_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'brandfetch', + pricing: { + type: 'per_request', + // Brand API: $99/month for 2,500 calls = $0.0396/request — https://docs.brandfetch.com/brand-api/quotas-and-usage + cost: 0.04, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/brandfetch/search.ts b/apps/sim/tools/brandfetch/search.ts index 2d2a73c5c5b..7feae21a492 100644 --- a/apps/sim/tools/brandfetch/search.ts +++ b/apps/sim/tools/brandfetch/search.ts @@ -11,6 +11,21 @@ export const brandfetchSearchTool: ToolConfig { + if (!Array.isArray(output.steps)) { + throw new Error('Browser Use response missing steps array, cannot determine cost') + } + const INIT_COST = 0.01 + const STEP_COSTS: Record = { + 'browser-use-llm': 0.002, + 'browser-use-2.0': 0.006, + o3: 0.03, + 'o4-mini': 0.03, + 'gemini-3-pro-preview': 0.03, + 'gemini-3-flash-preview': 0.015, + 'gemini-flash-latest': 0.0075, + 'gemini-flash-lite-latest': 0.005, + 'gemini-2.5-flash': 0.0075, + 'gemini-2.5-pro': 0.03, + 'claude-sonnet-4-5-20250929': 0.05, + 'claude-opus-4-5-20251101': 0.05, + 'claude-3-7-sonnet-20250219': 0.05, + 'gpt-4o': 0.006, + 'gpt-4o-mini': 0.006, + 'gpt-4.1': 0.006, + 'gpt-4.1-mini': 0.006, + 'llama-4-maverick-17b-128e-instruct': 0.006, + } + const DEFAULT_STEP_COST = 0.006 + const model = (params.model as string) || 'browser-use-2.0' + const knownCost = STEP_COSTS[model] + if (!knownCost) { + logger.warn( + `Unknown Browser Use model "${model}", using default step cost $${DEFAULT_STEP_COST}` + ) + PlatformEvents.hostedKeyUnknownModelCost({ + toolId: 'browser_use_run_task', + modelName: model, + defaultCost: DEFAULT_STEP_COST, + }) + } + const stepCost = knownCost ?? DEFAULT_STEP_COST + const stepCount = output.steps.length + const total = INIT_COST + stepCount * stepCost + return { cost: total, metadata: { model, stepCount, stepCost, initCost: INIT_COST } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { url: 'https://api.browser-use.com/api/v2/tasks', method: 'POST', diff --git a/apps/sim/tools/elevenlabs/constants.ts b/apps/sim/tools/elevenlabs/constants.ts new file mode 100644 index 00000000000..fc0dd62da42 --- /dev/null +++ b/apps/sim/tools/elevenlabs/constants.ts @@ -0,0 +1,5 @@ +export const FLASH_TURBO_MODELS = new Set([ + 'eleven_turbo_v2', + 'eleven_turbo_v2_5', + 'eleven_flash_v2_5', +]) diff --git a/apps/sim/tools/elevenlabs/tts.ts b/apps/sim/tools/elevenlabs/tts.ts index e0a42e010ca..bab31a5e3fd 100644 --- a/apps/sim/tools/elevenlabs/tts.ts +++ b/apps/sim/tools/elevenlabs/tts.ts @@ -1,3 +1,4 @@ +import { FLASH_TURBO_MODELS } from '@/tools/elevenlabs/constants' import type { ElevenLabsTtsParams, ElevenLabsTtsResponse } from '@/tools/elevenlabs/types' import type { ToolConfig } from '@/tools/types' @@ -7,6 +8,32 @@ export const elevenLabsTtsTool: ToolConfig { + const text = params.text as string | undefined + if (!text) { + throw new Error('Missing text parameter, cannot determine character cost') + } + const characterCount = text.length + const modelId = (params.modelId as string) || 'eleven_monolingual_v1' + // Flash/Turbo: $0.08/1K chars, Standard/Multilingual/v3: $0.18/1K chars + // Scale tier additional character rates — https://elevenlabs.io/pricing/api + const costPer1KChars = FLASH_TURBO_MODELS.has(modelId) ? 0.08 : 0.18 + const cost = (characterCount / 1000) * costPer1KChars + return { cost, metadata: { characterCount, modelId, costPer1KChars } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + params: { text: { type: 'string', diff --git a/apps/sim/tools/firecrawl/agent.ts b/apps/sim/tools/firecrawl/agent.ts index bd4b4f41e43..3ecddfbf373 100644 --- a/apps/sim/tools/firecrawl/agent.ts +++ b/apps/sim/tools/firecrawl/agent.ts @@ -55,6 +55,27 @@ export const agentTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl agent response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { method: 'POST', url: 'https://api.firecrawl.dev/v2/agent', diff --git a/apps/sim/tools/firecrawl/crawl.ts b/apps/sim/tools/firecrawl/crawl.ts index 04886632d7e..9d7d0105801 100644 --- a/apps/sim/tools/firecrawl/crawl.ts +++ b/apps/sim/tools/firecrawl/crawl.ts @@ -68,6 +68,28 @@ export const crawlTool: ToolConfig description: 'Firecrawl API Key', }, }, + + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl crawl response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { url: 'https://api.firecrawl.dev/v2/crawl', method: 'POST', diff --git a/apps/sim/tools/firecrawl/extract.ts b/apps/sim/tools/firecrawl/extract.ts index a82aa3eb249..97e763b814f 100644 --- a/apps/sim/tools/firecrawl/extract.ts +++ b/apps/sim/tools/firecrawl/extract.ts @@ -79,6 +79,27 @@ export const extractTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl extract response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { method: 'POST', url: 'https://api.firecrawl.dev/v2/extract', @@ -164,6 +185,7 @@ export const extractTool: ToolConfig = { jobId, success: true, data: extractData.data || {}, + creditsUsed: extractData.creditsUsed, } return result } @@ -211,5 +233,9 @@ export const extractTool: ToolConfig = { type: 'object', description: 'Extracted structured data according to the schema or prompt', }, + creditsUsed: { + type: 'number', + description: 'Number of Firecrawl credits consumed by the extraction', + }, }, } diff --git a/apps/sim/tools/firecrawl/map.ts b/apps/sim/tools/firecrawl/map.ts index 298cb25c1b4..b88b78a0a4f 100644 --- a/apps/sim/tools/firecrawl/map.ts +++ b/apps/sim/tools/firecrawl/map.ts @@ -66,6 +66,27 @@ export const mapTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl map response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { method: 'POST', url: 'https://api.firecrawl.dev/v2/map', @@ -100,6 +121,7 @@ export const mapTool: ToolConfig = { output: { success: data.success, links: data.links || [], + creditsUsed: data.creditsUsed, }, } }, @@ -116,5 +138,6 @@ export const mapTool: ToolConfig = { type: 'string', }, }, + creditsUsed: { type: 'number', description: 'Number of Firecrawl credits consumed' }, }, } diff --git a/apps/sim/tools/firecrawl/scrape.ts b/apps/sim/tools/firecrawl/scrape.ts index 6d1bfafa94a..d23f8483a2a 100644 --- a/apps/sim/tools/firecrawl/scrape.ts +++ b/apps/sim/tools/firecrawl/scrape.ts @@ -31,6 +31,27 @@ export const scrapeTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl scrape response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { method: 'POST', url: 'https://api.firecrawl.dev/v2/scrape', @@ -82,6 +103,7 @@ export const scrapeTool: ToolConfig = { markdown: data.data.markdown, html: data.data.html, metadata: data.data.metadata, + creditsUsed: data.creditsUsed, }, } }, @@ -94,5 +116,6 @@ export const scrapeTool: ToolConfig = { description: 'Page metadata including SEO and Open Graph information', properties: PAGE_METADATA_OUTPUT_PROPERTIES, }, + creditsUsed: { type: 'number', description: 'Number of Firecrawl credits consumed' }, }, } diff --git a/apps/sim/tools/firecrawl/search.ts b/apps/sim/tools/firecrawl/search.ts index 56cbc55608a..f5222985b24 100644 --- a/apps/sim/tools/firecrawl/search.ts +++ b/apps/sim/tools/firecrawl/search.ts @@ -23,6 +23,27 @@ export const searchTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl search response missing creditsUsed field') + } + const creditsUsed = output.creditsUsed as number + const cost = creditsUsed * 0.001 + return { cost, metadata: { creditsUsed } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { method: 'POST', url: 'https://api.firecrawl.dev/v2/search', @@ -58,6 +79,7 @@ export const searchTool: ToolConfig = { success: true, output: { data: data.data, + creditsUsed: data.creditsUsed, }, } }, @@ -71,5 +93,6 @@ export const searchTool: ToolConfig = { properties: SEARCH_RESULT_OUTPUT_PROPERTIES, }, }, + creditsUsed: { type: 'number', description: 'Number of Firecrawl credits consumed' }, }, } diff --git a/apps/sim/tools/firecrawl/types.ts b/apps/sim/tools/firecrawl/types.ts index 6fd7e3e5f5f..1853891ce47 100644 --- a/apps/sim/tools/firecrawl/types.ts +++ b/apps/sim/tools/firecrawl/types.ts @@ -448,6 +448,7 @@ export interface ScrapeResponse extends ToolResponse { statusCode: number error?: string } + creditsUsed: number } } @@ -470,6 +471,7 @@ export interface SearchResponse extends ToolResponse { error?: string } }> + creditsUsed: number } } @@ -500,6 +502,7 @@ export interface MapResponse extends ToolResponse { output: { success: boolean links: string[] + creditsUsed: number } } @@ -508,6 +511,7 @@ export interface ExtractResponse extends ToolResponse { jobId: string success: boolean data: Record + creditsUsed?: number } } diff --git a/apps/sim/tools/google_books/volume_details.ts b/apps/sim/tools/google_books/volume_details.ts index 23fe7cd4a47..799cfc74dd6 100644 --- a/apps/sim/tools/google_books/volume_details.ts +++ b/apps/sim/tools/google_books/volume_details.ts @@ -35,6 +35,20 @@ export const googleBooksVolumeDetailsTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + request: { url: (params) => { const url = new URL(`https://www.googleapis.com/books/v1/volumes/${params.volumeId.trim()}`) diff --git a/apps/sim/tools/google_books/volume_search.ts b/apps/sim/tools/google_books/volume_search.ts index 55d90cf9a36..10f2bed0259 100644 --- a/apps/sim/tools/google_books/volume_search.ts +++ b/apps/sim/tools/google_books/volume_search.ts @@ -93,6 +93,20 @@ export const googleBooksVolumeSearchTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + request: { url: (params) => { const url = new URL('https://www.googleapis.com/books/v1/volumes') diff --git a/apps/sim/tools/google_maps/air_quality.ts b/apps/sim/tools/google_maps/air_quality.ts index f50c14b920d..e9d0b276d04 100644 --- a/apps/sim/tools/google_maps/air_quality.ts +++ b/apps/sim/tools/google_maps/air_quality.ts @@ -40,6 +40,20 @@ export const googleMapsAirQualityTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { return `https://airquality.googleapis.com/v1/currentConditions:lookup?key=${params.apiKey.trim()}` diff --git a/apps/sim/tools/google_maps/directions.ts b/apps/sim/tools/google_maps/directions.ts index a3cdaa3ce9d..aa0544941e2 100644 --- a/apps/sim/tools/google_maps/directions.ts +++ b/apps/sim/tools/google_maps/directions.ts @@ -64,6 +64,20 @@ export const googleMapsDirectionsTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/directions/json') diff --git a/apps/sim/tools/google_maps/distance_matrix.ts b/apps/sim/tools/google_maps/distance_matrix.ts index ae5bf9c0ab4..19f981e8f4d 100644 --- a/apps/sim/tools/google_maps/distance_matrix.ts +++ b/apps/sim/tools/google_maps/distance_matrix.ts @@ -58,6 +58,20 @@ export const googleMapsDistanceMatrixTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/distancematrix/json') diff --git a/apps/sim/tools/google_maps/elevation.ts b/apps/sim/tools/google_maps/elevation.ts index 9a25eae3c0b..ea55447cd29 100644 --- a/apps/sim/tools/google_maps/elevation.ts +++ b/apps/sim/tools/google_maps/elevation.ts @@ -34,6 +34,20 @@ export const googleMapsElevationTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/elevation/json') diff --git a/apps/sim/tools/google_maps/geocode.ts b/apps/sim/tools/google_maps/geocode.ts index e48eb145f7f..e137e4abce1 100644 --- a/apps/sim/tools/google_maps/geocode.ts +++ b/apps/sim/tools/google_maps/geocode.ts @@ -35,6 +35,20 @@ export const googleMapsGeocodeTool: ToolConfig { const url = new URL('https://maps.googleapis.com/maps/api/geocode/json') diff --git a/apps/sim/tools/google_maps/geolocate.ts b/apps/sim/tools/google_maps/geolocate.ts index 423ff12e463..3af3fcd3786 100644 --- a/apps/sim/tools/google_maps/geolocate.ts +++ b/apps/sim/tools/google_maps/geolocate.ts @@ -66,6 +66,20 @@ export const googleMapsGeolocateTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { return `https://www.googleapis.com/geolocation/v1/geolocate?key=${params.apiKey.trim()}` diff --git a/apps/sim/tools/google_maps/place_details.ts b/apps/sim/tools/google_maps/place_details.ts index 763d23dc7e2..2da064ea309 100644 --- a/apps/sim/tools/google_maps/place_details.ts +++ b/apps/sim/tools/google_maps/place_details.ts @@ -40,6 +40,20 @@ export const googleMapsPlaceDetailsTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.017, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/place/details/json') diff --git a/apps/sim/tools/google_maps/places_search.ts b/apps/sim/tools/google_maps/places_search.ts index 82b01875685..9891114d0a5 100644 --- a/apps/sim/tools/google_maps/places_search.ts +++ b/apps/sim/tools/google_maps/places_search.ts @@ -58,6 +58,20 @@ export const googleMapsPlacesSearchTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.032, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/place/textsearch/json') diff --git a/apps/sim/tools/google_maps/reverse_geocode.ts b/apps/sim/tools/google_maps/reverse_geocode.ts index 881ffa31cff..27f85d1a929 100644 --- a/apps/sim/tools/google_maps/reverse_geocode.ts +++ b/apps/sim/tools/google_maps/reverse_geocode.ts @@ -41,6 +41,20 @@ export const googleMapsReverseGeocodeTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/geocode/json') diff --git a/apps/sim/tools/google_maps/snap_to_roads.ts b/apps/sim/tools/google_maps/snap_to_roads.ts index 0246a81e1cb..90554bcde76 100644 --- a/apps/sim/tools/google_maps/snap_to_roads.ts +++ b/apps/sim/tools/google_maps/snap_to_roads.ts @@ -35,6 +35,20 @@ export const googleMapsSnapToRoadsTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.01, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://roads.googleapis.com/v1/snapToRoads') diff --git a/apps/sim/tools/google_maps/speed_limits.ts b/apps/sim/tools/google_maps/speed_limits.ts index b0fdc2e49f4..581baf50538 100644 --- a/apps/sim/tools/google_maps/speed_limits.ts +++ b/apps/sim/tools/google_maps/speed_limits.ts @@ -34,6 +34,20 @@ export const googleMapsSpeedLimitsTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.02, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const hasPath = params.path && params.path.trim().length > 0 diff --git a/apps/sim/tools/google_maps/timezone.ts b/apps/sim/tools/google_maps/timezone.ts index d0dcef7270e..7b96c2b934d 100644 --- a/apps/sim/tools/google_maps/timezone.ts +++ b/apps/sim/tools/google_maps/timezone.ts @@ -46,6 +46,20 @@ export const googleMapsTimezoneTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://maps.googleapis.com/maps/api/timezone/json') diff --git a/apps/sim/tools/google_maps/validate_address.ts b/apps/sim/tools/google_maps/validate_address.ts index bea8863ec1d..02d5fb76744 100644 --- a/apps/sim/tools/google_maps/validate_address.ts +++ b/apps/sim/tools/google_maps/validate_address.ts @@ -46,6 +46,20 @@ export const googleMapsValidateAddressTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { return `https://addressvalidation.googleapis.com/v1:validateAddress?key=${params.apiKey.trim()}` diff --git a/apps/sim/tools/google_pagespeed/analyze.ts b/apps/sim/tools/google_pagespeed/analyze.ts index a5fc0cfa5e2..56d8bf3c375 100644 --- a/apps/sim/tools/google_pagespeed/analyze.ts +++ b/apps/sim/tools/google_pagespeed/analyze.ts @@ -46,6 +46,20 @@ export const analyzeTool: ToolConfig { const url = new URL('https://www.googleapis.com/pagespeedonline/v5/runPagespeed') diff --git a/apps/sim/tools/google_translate/detect.ts b/apps/sim/tools/google_translate/detect.ts index 48941276f35..d25c5c71063 100644 --- a/apps/sim/tools/google_translate/detect.ts +++ b/apps/sim/tools/google_translate/detect.ts @@ -28,6 +28,29 @@ export const googleTranslateDetectTool: ToolConfig< }, }, + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'custom', + getCost: (params) => { + const text = params.text as string + if (!text) { + throw new Error('Missing text parameter, cannot determine detection cost') + } + // $20 per 1M characters — from https://cloud.google.com/translate/pricing + const charCount = text.length + const cost = charCount * 0.00002 + return { cost, metadata: { characters: charCount } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://translation.googleapis.com/language/translate/v2/detect') diff --git a/apps/sim/tools/google_translate/text.ts b/apps/sim/tools/google_translate/text.ts index 8f74df318cb..e0b793c9f50 100644 --- a/apps/sim/tools/google_translate/text.ts +++ b/apps/sim/tools/google_translate/text.ts @@ -42,6 +42,29 @@ export const googleTranslateTool: ToolConfig { + const text = params.text as string + if (!text) { + throw new Error('Missing text parameter, cannot determine translation cost') + } + // $20 per 1M characters — from https://cloud.google.com/translate/pricing + const charCount = text.length + const cost = charCount * 0.00002 + return { cost, metadata: { characters: charCount } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: (params) => { const url = new URL('https://translation.googleapis.com/language/translate/v2') diff --git a/apps/sim/tools/huggingface/chat.ts b/apps/sim/tools/huggingface/chat.ts index 0c6bb2c030b..ac696302d79 100644 --- a/apps/sim/tools/huggingface/chat.ts +++ b/apps/sim/tools/huggingface/chat.ts @@ -128,17 +128,7 @@ export const chatTool: ToolConfig( if (!isRateLimitError(error) || attempt === maxRetries) { if (isRateLimitError(error) && attempt === maxRetries) { - PlatformEvents.userThrottled({ + PlatformEvents.hostedKeyUserThrottled({ toolId, reason: 'upstream_retries_exhausted', userId: executionContext?.userId, @@ -277,7 +285,7 @@ async function processHostedKeyCost( if (!userId) return { cost, metadata } - const skipLog = !!ctx?.skipFixedUsageLog + const skipLog = !!ctx?.skipFixedUsageLog || !!tool.hosting?.skipFixedUsageLog if (!skipLog) { try { await logFixedUsage({ @@ -377,6 +385,13 @@ async function applyHostedKeyCostToResult( ): Promise { await reportCustomDimensionUsage(tool, params, finalResult.output, executionContext, requestId) + if (tool.hosting?.skipFixedUsageLog) { + const ctx = params._context as Record | undefined + if (ctx) { + ctx.skipFixedUsageLog = true + } + } + const { cost: hostedKeyCost, metadata } = await processHostedKeyCost( tool, params, diff --git a/apps/sim/tools/jina/read_url.ts b/apps/sim/tools/jina/read_url.ts index a801143dd30..c7c4b31029f 100644 --- a/apps/sim/tools/jina/read_url.ts +++ b/apps/sim/tools/jina/read_url.ts @@ -159,25 +159,58 @@ export const readUrlTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'JINA_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'jina', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.tokensUsed == null) { + throw new Error('Jina read_url response missing tokensUsed field') + } + // Jina bills per output token — $0.20 per 1M tokens + // Source: https://cloud.jina.ai/pricing (token-based billing) + const tokens = output.tokensUsed as number + const cost = tokens * 0.0000002 + return { cost, metadata: { tokensUsed: tokens } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 200, + }, + }, + transformResponse: async (response: Response) => { + let tokensUsed: number | undefined + + const tokensHeader = response.headers.get('x-tokens') + if (tokensHeader) { + const parsed = Number.parseInt(tokensHeader, 10) + if (!Number.isNaN(parsed)) { + tokensUsed = parsed + } + } + const contentType = response.headers.get('content-type') if (contentType?.includes('application/json')) { const data = await response.json() + tokensUsed ??= data.data?.usage?.tokens ?? data.usage?.tokens + const content = data.data?.content || data.content || JSON.stringify(data) + tokensUsed ??= Math.ceil(content.length / 4) return { success: response.ok, - output: { - content: data.data?.content || data.content || JSON.stringify(data), - }, + output: { content, tokensUsed }, } } const content = await response.text() + tokensUsed ??= Math.ceil(content.length / 4) return { success: response.ok, - output: { - content, - }, + output: { content, tokensUsed }, } }, @@ -186,5 +219,10 @@ export const readUrlTool: ToolConfig = { type: 'string', description: 'The extracted content from the URL, processed into clean, LLM-friendly text', }, + tokensUsed: { + type: 'number', + description: 'Number of Jina tokens consumed by this request', + optional: true, + }, }, } diff --git a/apps/sim/tools/jina/search.ts b/apps/sim/tools/jina/search.ts index f1009e355f9..158893e4ec6 100644 --- a/apps/sim/tools/jina/search.ts +++ b/apps/sim/tools/jina/search.ts @@ -147,12 +147,65 @@ export const searchTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'JINA_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'jina', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.tokensUsed == null) { + throw new Error('Jina search response missing tokensUsed field') + } + // Jina bills per output token — $0.20 per 1M tokens + // Search costs a fixed minimum of 10,000 tokens per request + // Source: https://cloud.jina.ai/pricing (token-based billing) + // x-tokens header is unreliable; falls back to content-length estimate (~4 chars/token) + const tokens = output.tokensUsed as number + const cost = tokens * 0.0000002 + return { cost, metadata: { tokensUsed: tokens } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 40, + }, + }, + transformResponse: async (response: Response) => { const data = await response.json() - // The API returns an array of results or a data object with results + let tokensUsed: number | undefined + const tokensHeader = response.headers.get('x-tokens') + if (tokensHeader) { + const parsed = Number.parseInt(tokensHeader, 10) + if (!Number.isNaN(parsed) && parsed > 0) { + tokensUsed = parsed + } + } + const results = Array.isArray(data) ? data : data.data || [] + if (tokensUsed == null) { + let total = 0 + for (const result of results) { + if (result.usage?.tokens) { + total += result.usage.tokens + } + } + if (total > 0) { + tokensUsed = total + } + } + + if (tokensUsed == null) { + let totalChars = 0 + for (const result of results) { + totalChars += (result.content?.length ?? 0) + (result.title?.length ?? 0) + } + tokensUsed = Math.max(Math.ceil(totalChars / 4), 10000) + } + return { success: response.ok, output: { @@ -162,6 +215,7 @@ export const searchTool: ToolConfig = { url: result.url || '', content: result.content || '', })), + tokensUsed, }, } }, @@ -176,5 +230,10 @@ export const searchTool: ToolConfig = { properties: JINA_SEARCH_RESULT_OUTPUT_PROPERTIES, }, }, + tokensUsed: { + type: 'number', + description: 'Number of Jina tokens consumed by this request', + optional: true, + }, }, } diff --git a/apps/sim/tools/linkup/search.ts b/apps/sim/tools/linkup/search.ts index 9a49dc9048d..27b063984bb 100644 --- a/apps/sim/tools/linkup/search.ts +++ b/apps/sim/tools/linkup/search.ts @@ -82,6 +82,27 @@ export const searchTool: ToolConfig { + // Linkup pricing (https://docs.linkup.so/pages/documentation/development/pricing): + // Standard: €0.005/call ≈ $0.006 + // Deep: €0.05/call ≈ $0.055 + const depth = params.depth as string + const cost = depth === 'deep' ? 0.055 : 0.006 + return { cost, metadata: { depth } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + request: { url: 'https://api.linkup.so/v1/search', method: 'POST', diff --git a/apps/sim/tools/mistral/parser.ts b/apps/sim/tools/mistral/parser.ts index 44c80dd8a6c..c2727c235df 100644 --- a/apps/sim/tools/mistral/parser.ts +++ b/apps/sim/tools/mistral/parser.ts @@ -10,6 +10,31 @@ import type { ToolConfig } from '@/tools/types' const logger = createLogger('MistralParserTool') +const MISTRAL_OCR_HOSTING = { + envKeyPrefix: 'MISTRAL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'mistral' as const, + pricing: { + type: 'custom' as const, + getCost: (_params: Record, output: Record) => { + // Mistral OCR: $2 per 1,000 pages ($0.002/page) for real-time API + // Batch inference is $1/1K pages but we call the real-time endpoint + // https://mistral.ai/pricing#api + const usageInfo = output.usage_info as { pages_processed?: number } | undefined + if (usageInfo?.pages_processed == null) { + throw new Error('Mistral OCR response missing pages_processed in usage_info') + } + const pagesProcessed = usageInfo.pages_processed + const cost = pagesProcessed * 0.002 + return { cost, metadata: { pagesProcessed } } + }, + }, + rateLimit: { + mode: 'per_request' as const, + requestsPerMinute: 60, + }, +} + export const mistralParserTool: ToolConfig = { id: 'mistral_parser', name: 'Mistral PDF Parser', @@ -492,6 +517,7 @@ export const mistralParserV3Tool: ToolConfig { + // Parallel Task API: cost varies by processor + // https://docs.parallel.ai/resources/pricing + const processorCosts: Record = { + lite: 0.005, + base: 0.01, + core: 0.025, + core2x: 0.05, + pro: 0.1, + ultra: 0.3, + ultra2x: 0.6, + ultra4x: 1.2, + ultra8x: 2.4, + } + const processor = (params.processor as string) || 'base' + const DEFAULT_PROCESSOR_COST = processorCosts.base + const knownCost = processorCosts[processor] + if (knownCost == null) { + logger.warn( + `Unknown Parallel processor "${processor}", using default processor cost $${DEFAULT_PROCESSOR_COST}` + ) + PlatformEvents.hostedKeyUnknownModelCost({ + toolId: 'parallel_deep_research', + modelName: processor, + defaultCost: DEFAULT_PROCESSOR_COST, + }) + } + const cost = knownCost ?? DEFAULT_PROCESSOR_COST + return { cost, metadata: { processor, defaultProcessorCost: DEFAULT_PROCESSOR_COST } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 10, + }, + }, + params: { input: { type: 'string', diff --git a/apps/sim/tools/parallel/extract.ts b/apps/sim/tools/parallel/extract.ts index 196d3eb299d..51445c24a96 100644 --- a/apps/sim/tools/parallel/extract.ts +++ b/apps/sim/tools/parallel/extract.ts @@ -8,6 +8,29 @@ export const extractTool: ToolConfig = { 'Extract targeted information from specific URLs using Parallel AI. Processes provided URLs to pull relevant content based on your objective.', version: '1.0.0', + hosting: { + envKeyPrefix: 'PARALLEL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'parallel_ai', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (!Array.isArray(output.results)) { + throw new Error('Parallel extract response missing results array') + } + // Parallel Extract: $1 per 1,000 URLs = $0.001 per URL + // https://docs.parallel.ai/resources/pricing + const urlCount = output.results.length + const cost = urlCount * 0.001 + return { cost, metadata: { urlCount } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + params: { urls: { type: 'string', diff --git a/apps/sim/tools/parallel/search.ts b/apps/sim/tools/parallel/search.ts index 6cd919a1404..d84ac5ed555 100644 --- a/apps/sim/tools/parallel/search.ts +++ b/apps/sim/tools/parallel/search.ts @@ -8,6 +8,30 @@ export const searchTool: ToolConfig = { 'Search the web using Parallel AI. Provides comprehensive search results with intelligent processing and content extraction.', version: '1.0.0', + hosting: { + envKeyPrefix: 'PARALLEL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'parallel_ai', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (!Array.isArray(output.results)) { + throw new Error('Parallel search response missing results array') + } + // Parallel Search: $0.005 base (includes ≤10 results), +$0.001 per result beyond 10 + // https://docs.parallel.ai/resources/pricing + const resultCount = output.results.length + const additionalResults = Math.max(0, resultCount - 10) + const cost = 0.005 + additionalResults * 0.001 + return { cost, metadata: { resultCount, additionalResults } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + params: { objective: { type: 'string', diff --git a/apps/sim/tools/perplexity/chat.ts b/apps/sim/tools/perplexity/chat.ts index 5985c3354b3..28940e1b101 100644 --- a/apps/sim/tools/perplexity/chat.ts +++ b/apps/sim/tools/perplexity/chat.ts @@ -1,6 +1,42 @@ import type { PerplexityChatParams, PerplexityChatResponse } from '@/tools/perplexity/types' import type { ToolConfig } from '@/tools/types' +/** + * Per-token rates by model from https://docs.perplexity.ai/guides/pricing + * Per-request fees assume Low context size (the API default). + * Deep Research has additional billing dimensions: citation tokens, search queries, reasoning tokens. + */ +const MODEL_PRICING: Record< + string, + { + inputPerM: number + outputPerM: number + requestPer1K: number + citationPerM?: number + searchQueriesPer1K?: number + reasoningPerM?: number + } +> = { + 'sonar-deep-research': { + inputPerM: 2, + outputPerM: 8, + requestPer1K: 0, + citationPerM: 2, + searchQueriesPer1K: 5, + reasoningPerM: 3, + }, + 'sonar-reasoning-pro': { inputPerM: 2, outputPerM: 8, requestPer1K: 6 }, + 'sonar-pro': { inputPerM: 3, outputPerM: 15, requestPer1K: 6 }, + sonar: { inputPerM: 1, outputPerM: 1, requestPer1K: 5 }, +} + +function getModelPricing(model: string) { + for (const [key, pricing] of Object.entries(MODEL_PRICING)) { + if (model.includes(key)) return pricing + } + return MODEL_PRICING.sonar +} + export const chatTool: ToolConfig = { id: 'perplexity_chat', name: 'Perplexity Chat', @@ -48,6 +84,77 @@ export const chatTool: ToolConfig }, }, + hosting: { + envKeyPrefix: 'PERPLEXITY_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'perplexity', + pricing: { + type: 'custom', + getCost: (params, output) => { + const usage = output.usage as + | { + prompt_tokens?: number + completion_tokens?: number + citation_tokens?: number + num_search_queries?: number + reasoning_tokens?: number + } + | undefined + if (!usage || usage.prompt_tokens == null || usage.completion_tokens == null) { + throw new Error('Perplexity chat response missing token usage data') + } + + const model = ((output.model as string) || params.model) as string + const pricing = getModelPricing(model) + const inputTokens = usage.prompt_tokens + const outputTokens = usage.completion_tokens + + let tokenCost = + (inputTokens * pricing.inputPerM) / 1_000_000 + + (outputTokens * pricing.outputPerM) / 1_000_000 + const requestFee = pricing.requestPer1K / 1000 + + let citationCost = 0 + let searchQueryCost = 0 + let reasoningCost = 0 + + if (pricing.citationPerM && usage.citation_tokens) { + citationCost = (usage.citation_tokens * pricing.citationPerM) / 1_000_000 + } + if (pricing.searchQueriesPer1K && usage.num_search_queries) { + searchQueryCost = (usage.num_search_queries * pricing.searchQueriesPer1K) / 1000 + } + if (pricing.reasoningPerM && usage.reasoning_tokens) { + reasoningCost = (usage.reasoning_tokens * pricing.reasoningPerM) / 1_000_000 + } + + const cost = tokenCost + requestFee + citationCost + searchQueryCost + reasoningCost + + return { + cost, + metadata: { + model, + inputTokens, + outputTokens, + tokenCost, + requestFee, + citationTokens: usage.citation_tokens, + citationCost, + searchQueries: usage.num_search_queries, + searchQueryCost, + reasoningTokens: usage.reasoning_tokens, + reasoningCost, + }, + } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 20, + }, + skipFixedUsageLog: true, + }, + request: { method: 'POST', url: () => 'https://api.perplexity.ai/chat/completions', @@ -101,6 +208,15 @@ export const chatTool: ToolConfig prompt_tokens: data.usage.prompt_tokens, completion_tokens: data.usage.completion_tokens, total_tokens: data.usage.total_tokens, + ...(data.usage.citation_tokens != null && { + citation_tokens: data.usage.citation_tokens, + }), + ...(data.usage.num_search_queries != null && { + num_search_queries: data.usage.num_search_queries, + }), + ...(data.usage.reasoning_tokens != null && { + reasoning_tokens: data.usage.reasoning_tokens, + }), }, }, } diff --git a/apps/sim/tools/perplexity/search.ts b/apps/sim/tools/perplexity/search.ts index bdc4b55f6ca..7a1e357baf9 100644 --- a/apps/sim/tools/perplexity/search.ts +++ b/apps/sim/tools/perplexity/search.ts @@ -66,6 +66,20 @@ export const searchTool: ToolConfig 'https://api.perplexity.ai/search', diff --git a/apps/sim/tools/perplexity/types.ts b/apps/sim/tools/perplexity/types.ts index 4ac8549c78f..495806f20e2 100644 --- a/apps/sim/tools/perplexity/types.ts +++ b/apps/sim/tools/perplexity/types.ts @@ -22,6 +22,9 @@ export interface PerplexityChatResponse extends ToolResponse { prompt_tokens: number completion_tokens: number total_tokens: number + citation_tokens?: number + num_search_queries?: number + reasoning_tokens?: number } } } diff --git a/apps/sim/tools/serper/search.ts b/apps/sim/tools/serper/search.ts index 685c2b64339..9b14d3461bf 100644 --- a/apps/sim/tools/serper/search.ts +++ b/apps/sim/tools/serper/search.ts @@ -49,6 +49,28 @@ export const searchTool: ToolConfig = { }, }, + hosting: { + envKeyPrefix: 'SERPER_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'serper', + pricing: { + type: 'custom', + getCost: (params, output) => { + if (!Array.isArray(output.searchResults)) { + throw new Error('Serper response missing searchResults, cannot determine cost') + } + const num = Number(params.num) || 10 + const credits = num > 10 ? 2 : 1 + const cost = credits * 0.001 + return { cost, metadata: { num, credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + request: { url: (params) => `https://google.serper.dev/${params.type || 'search'}`, method: 'POST', diff --git a/apps/sim/tools/tts/elevenlabs.ts b/apps/sim/tools/tts/elevenlabs.ts index 6a177a5f503..fa4729ac741 100644 --- a/apps/sim/tools/tts/elevenlabs.ts +++ b/apps/sim/tools/tts/elevenlabs.ts @@ -1,3 +1,4 @@ +import { FLASH_TURBO_MODELS } from '@/tools/elevenlabs/constants' import type { ElevenLabsTtsUnifiedParams, TtsBlockResponse } from '@/tools/tts/types' import type { ToolConfig } from '@/tools/types' @@ -7,6 +8,32 @@ export const elevenLabsTtsUnifiedTool: ToolConfig { + const text = params.text as string | undefined + if (!text) { + throw new Error('Missing text parameter, cannot determine character cost') + } + const characterCount = text.length + const modelId = (params.modelId as string) || 'eleven_turbo_v2_5' + // Flash/Turbo: $0.08/1K chars, Standard/Multilingual/v3: $0.18/1K chars + // Scale tier additional character rates — https://elevenlabs.io/pricing/api + const costPer1KChars = FLASH_TURBO_MODELS.has(modelId) ? 0.08 : 0.18 + const cost = (characterCount / 1000) * costPer1KChars + return { cost, metadata: { characterCount, modelId, costPer1KChars } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 30, + }, + }, + params: { text: { type: 'string', diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index 0648b643be7..e491b12b8b4 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -1,7 +1,22 @@ import type { HostedKeyRateLimitConfig } from '@/lib/core/rate-limiter' import type { OAuthService } from '@/lib/oauth' -export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'exa' +export type BYOKProviderId = + | 'openai' + | 'anthropic' + | 'google' + | 'mistral' + | 'exa' + | 'browser_use' + | 'serper' + | 'firecrawl' + | 'jina' + | 'elevenlabs' + | 'perplexity' + | 'google_cloud' + | 'linkup' + | 'brandfetch' + | 'parallel_ai' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' @@ -299,4 +314,6 @@ export interface ToolHostingConfig

> { pricing: ToolHostingPricing

/** Hosted key rate limit configuration (required for hosted key distribution) */ rateLimit: HostedKeyRateLimitConfig + /** When true, skip the fixed usage log entry (useful for tools that log custom dimensions instead) */ + skipFixedUsageLog?: boolean }