mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-14 00:15:09 -05:00
Add custom pricing, switch to exa as first hosted key
This commit is contained in:
@@ -12,7 +12,7 @@ import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/per
|
|||||||
|
|
||||||
const logger = createLogger('WorkspaceBYOKKeysAPI')
|
const logger = createLogger('WorkspaceBYOKKeysAPI')
|
||||||
|
|
||||||
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'serper'] as const
|
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'serper', 'exa'] as const
|
||||||
|
|
||||||
const UpsertKeySchema = z.object({
|
const UpsertKeySchema = z.object({
|
||||||
providerId: z.enum(VALID_PROVIDERS),
|
providerId: z.enum(VALID_PROVIDERS),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
import { AnthropicIcon, GeminiIcon, MistralIcon, OpenAIIcon, SerperIcon } from '@/components/icons'
|
import { AnthropicIcon, ExaAIIcon, GeminiIcon, MistralIcon, OpenAIIcon } from '@/components/icons'
|
||||||
import { Skeleton } from '@/components/ui'
|
import { Skeleton } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
type BYOKKey,
|
type BYOKKey,
|
||||||
@@ -61,11 +61,11 @@ const PROVIDERS: {
|
|||||||
placeholder: 'Enter your API key',
|
placeholder: 'Enter your API key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'serper',
|
id: 'exa',
|
||||||
name: 'Serper',
|
name: 'Exa',
|
||||||
icon: SerperIcon,
|
icon: ExaAIIcon,
|
||||||
description: 'Web search tool',
|
description: 'AI-powered search and research',
|
||||||
placeholder: 'Enter your Serper API key',
|
placeholder: 'Enter your Exa API key',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
|||||||
placeholder: 'Enter your Exa API key',
|
placeholder: 'Enter your Exa API key',
|
||||||
password: true,
|
password: true,
|
||||||
required: true,
|
required: true,
|
||||||
|
hideWhenHosted: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tools: {
|
tools: {
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export const SerperBlock: BlockConfig<SearchResponse> = {
|
|||||||
placeholder: 'Enter your Serper API key',
|
placeholder: 'Enter your Serper API key',
|
||||||
password: true,
|
password: true,
|
||||||
required: true,
|
required: true,
|
||||||
hideWhenHosted: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tools: {
|
tools: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { API_ENDPOINTS } from '@/stores/constants'
|
|||||||
|
|
||||||
const logger = createLogger('BYOKKeysQueries')
|
const logger = createLogger('BYOKKeysQueries')
|
||||||
|
|
||||||
export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'serper'
|
export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'exa'
|
||||||
|
|
||||||
export interface BYOKKey {
|
export interface BYOKKey {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useProvidersStore } from '@/stores/providers/store'
|
|||||||
|
|
||||||
const logger = createLogger('BYOKKeys')
|
const logger = createLogger('BYOKKeys')
|
||||||
|
|
||||||
export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'serper'
|
export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'exa'
|
||||||
|
|
||||||
export interface BYOKKeyResult {
|
export interface BYOKKeyResult {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export interface ModelUsageMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata for 'fixed' category charges (currently empty, extensible)
|
* Metadata for 'fixed' category charges (e.g., tool cost breakdown)
|
||||||
*/
|
*/
|
||||||
export type FixedUsageMetadata = Record<string, never>
|
export type FixedUsageMetadata = Record<string, unknown>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union type for all metadata types
|
* Union type for all metadata types
|
||||||
@@ -60,6 +60,8 @@ export interface LogFixedUsageParams {
|
|||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
workflowId?: string
|
workflowId?: string
|
||||||
executionId?: string
|
executionId?: string
|
||||||
|
/** Optional metadata (e.g., tool cost breakdown from API) */
|
||||||
|
metadata?: FixedUsageMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,7 +121,7 @@ export async function logFixedUsage(params: LogFixedUsageParams): Promise<void>
|
|||||||
category: 'fixed',
|
category: 'fixed',
|
||||||
source: params.source,
|
source: params.source,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
metadata: null,
|
metadata: params.metadata ?? null,
|
||||||
cost: params.cost.toString(),
|
cost: params.cost.toString(),
|
||||||
workspaceId: params.workspaceId ?? null,
|
workspaceId: params.workspaceId ?? null,
|
||||||
workflowId: params.workflowId ?? null,
|
workflowId: params.workflowId ?? null,
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ export const isTest = env.NODE_ENV === 'test'
|
|||||||
/**
|
/**
|
||||||
* Is this the hosted version of the application
|
* Is this the hosted version of the application
|
||||||
*/
|
*/
|
||||||
export const isHosted =
|
export const isHosted = true
|
||||||
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
|
// getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
|
||||||
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
|
// getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is billing enforcement enabled
|
* Is billing enforcement enabled
|
||||||
|
|||||||
@@ -27,6 +27,22 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
|
|||||||
description: 'Exa AI API Key',
|
description: 'Exa AI API Key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hosting: {
|
||||||
|
envKeys: ['EXA_API_KEY'],
|
||||||
|
apiKeyParam: 'apiKey',
|
||||||
|
byokProviderId: 'exa',
|
||||||
|
pricing: {
|
||||||
|
type: 'custom',
|
||||||
|
getCost: (_params, response) => {
|
||||||
|
// Use costDollars from Exa API response
|
||||||
|
if (response.costDollars?.total) {
|
||||||
|
return { cost: response.costDollars.total, metadata: { costDollars: response.costDollars } }
|
||||||
|
}
|
||||||
|
// Fallback: $5/1000 requests
|
||||||
|
return 0.005
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: 'https://api.exa.ai/answer',
|
url: 'https://api.exa.ai/answer',
|
||||||
@@ -61,6 +77,7 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
|
|||||||
url: citation.url,
|
url: citation.url,
|
||||||
text: citation.text || '',
|
text: citation.text || '',
|
||||||
})) || [],
|
})) || [],
|
||||||
|
costDollars: data.costDollars,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,6 +76,23 @@ export const findSimilarLinksTool: ToolConfig<
|
|||||||
description: 'Exa AI API Key',
|
description: 'Exa AI API Key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hosting: {
|
||||||
|
envKeys: ['EXA_API_KEY'],
|
||||||
|
apiKeyParam: 'apiKey',
|
||||||
|
byokProviderId: 'exa',
|
||||||
|
pricing: {
|
||||||
|
type: 'custom',
|
||||||
|
getCost: (_params, response) => {
|
||||||
|
// Use costDollars from Exa API response
|
||||||
|
if (response.costDollars?.total) {
|
||||||
|
return { cost: response.costDollars.total, metadata: { costDollars: response.costDollars } }
|
||||||
|
}
|
||||||
|
// Fallback: $5/1000 (1-25 results) or $25/1000 (26-100 results)
|
||||||
|
const resultCount = response.similarLinks?.length || 0
|
||||||
|
return resultCount <= 25 ? 0.005 : 0.025
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: 'https://api.exa.ai/findSimilar',
|
url: 'https://api.exa.ai/findSimilar',
|
||||||
@@ -140,6 +157,7 @@ export const findSimilarLinksTool: ToolConfig<
|
|||||||
highlights: result.highlights,
|
highlights: result.highlights,
|
||||||
score: result.score || 0,
|
score: result.score || 0,
|
||||||
})),
|
})),
|
||||||
|
costDollars: data.costDollars,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,22 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
|
|||||||
description: 'Exa AI API Key',
|
description: 'Exa AI API Key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hosting: {
|
||||||
|
envKeys: ['EXA_API_KEY'],
|
||||||
|
apiKeyParam: 'apiKey',
|
||||||
|
byokProviderId: 'exa',
|
||||||
|
pricing: {
|
||||||
|
type: 'custom',
|
||||||
|
getCost: (_params, response) => {
|
||||||
|
// Use costDollars from Exa API response
|
||||||
|
if (response.costDollars?.total) {
|
||||||
|
return { cost: response.costDollars.total, metadata: { costDollars: response.costDollars } }
|
||||||
|
}
|
||||||
|
// Fallback: $1/1000 pages
|
||||||
|
return (response.results?.length || 0) * 0.001
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: 'https://api.exa.ai/contents',
|
url: 'https://api.exa.ai/contents',
|
||||||
@@ -132,6 +148,7 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
|
|||||||
summary: result.summary || '',
|
summary: result.summary || '',
|
||||||
highlights: result.highlights,
|
highlights: result.highlights,
|
||||||
})),
|
})),
|
||||||
|
costDollars: data.costDollars,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
|
|||||||
description: 'Exa AI API Key',
|
description: 'Exa AI API Key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hosting: {
|
||||||
|
envKeys: ['EXA_API_KEY'],
|
||||||
|
apiKeyParam: 'apiKey',
|
||||||
|
byokProviderId: 'exa',
|
||||||
|
pricing: {
|
||||||
|
type: 'custom',
|
||||||
|
getCost: (params, response) => {
|
||||||
|
// Use costDollars from Exa API response
|
||||||
|
if (response.costDollars?.total) {
|
||||||
|
return { cost: response.costDollars.total, metadata: { costDollars: response.costDollars } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to estimate if cost not available
|
||||||
|
const model = params.model || 'exa-research'
|
||||||
|
return model === 'exa-research-pro' ? 0.055 : 0.03
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: 'https://api.exa.ai/research/v1',
|
url: 'https://api.exa.ai/research/v1',
|
||||||
@@ -111,6 +129,8 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
|
|||||||
score: 1.0,
|
score: 1.0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// Include cost breakdown for pricing calculation
|
||||||
|
costDollars: taskData.costDollars,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,28 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
|
|||||||
description: 'Exa AI API Key',
|
description: 'Exa AI API Key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hosting: {
|
||||||
|
envKeys: ['EXA_API_KEY'],
|
||||||
|
apiKeyParam: 'apiKey',
|
||||||
|
byokProviderId: 'exa',
|
||||||
|
pricing: {
|
||||||
|
type: 'custom',
|
||||||
|
getCost: (params, response) => {
|
||||||
|
// Use costDollars from Exa API response
|
||||||
|
if (response.costDollars?.total) {
|
||||||
|
return { cost: response.costDollars.total, metadata: { costDollars: response.costDollars } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: estimate based on search type and result count
|
||||||
|
const isDeepSearch = params.type === 'neural'
|
||||||
|
if (isDeepSearch) {
|
||||||
|
return 0.015
|
||||||
|
}
|
||||||
|
const resultCount = response.results?.length || 0
|
||||||
|
return resultCount <= 25 ? 0.005 : 0.025
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: 'https://api.exa.ai/search',
|
url: 'https://api.exa.ai/search',
|
||||||
@@ -167,6 +189,7 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
|
|||||||
highlights: result.highlights,
|
highlights: result.highlights,
|
||||||
score: result.score,
|
score: result.score,
|
||||||
})),
|
})),
|
||||||
|
costDollars: data.costDollars,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export interface ExaBaseParams {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cost breakdown returned by Exa API responses */
|
||||||
|
export interface ExaCostDollars {
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
// Search tool types
|
// Search tool types
|
||||||
export interface ExaSearchParams extends ExaBaseParams {
|
export interface ExaSearchParams extends ExaBaseParams {
|
||||||
query: string
|
query: string
|
||||||
@@ -50,6 +55,7 @@ export interface ExaSearchResult {
|
|||||||
export interface ExaSearchResponse extends ToolResponse {
|
export interface ExaSearchResponse extends ToolResponse {
|
||||||
output: {
|
output: {
|
||||||
results: ExaSearchResult[]
|
results: ExaSearchResult[]
|
||||||
|
costDollars?: ExaCostDollars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +84,7 @@ export interface ExaGetContentsResult {
|
|||||||
export interface ExaGetContentsResponse extends ToolResponse {
|
export interface ExaGetContentsResponse extends ToolResponse {
|
||||||
output: {
|
output: {
|
||||||
results: ExaGetContentsResult[]
|
results: ExaGetContentsResult[]
|
||||||
|
costDollars?: ExaCostDollars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +127,7 @@ export interface ExaSimilarLink {
|
|||||||
export interface ExaFindSimilarLinksResponse extends ToolResponse {
|
export interface ExaFindSimilarLinksResponse extends ToolResponse {
|
||||||
output: {
|
output: {
|
||||||
similarLinks: ExaSimilarLink[]
|
similarLinks: ExaSimilarLink[]
|
||||||
|
costDollars?: ExaCostDollars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +145,7 @@ export interface ExaAnswerResponse extends ToolResponse {
|
|||||||
url: string
|
url: string
|
||||||
text: string
|
text: string
|
||||||
}[]
|
}[]
|
||||||
|
costDollars?: ExaCostDollars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +167,7 @@ export interface ExaResearchResponse extends ToolResponse {
|
|||||||
author?: string
|
author?: string
|
||||||
score: number
|
score: number
|
||||||
}[]
|
}[]
|
||||||
|
costDollars?: ExaCostDollars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ async function injectHostedKeyIfNeeded(
|
|||||||
try {
|
try {
|
||||||
const byokResult = await getBYOKKey(
|
const byokResult = await getBYOKKey(
|
||||||
executionContext.workspaceId,
|
executionContext.workspaceId,
|
||||||
byokProviderId as 'openai' | 'anthropic' | 'google' | 'mistral' | 'serper'
|
byokProviderId as 'openai' | 'anthropic' | 'google' | 'mistral' | 'exa'
|
||||||
)
|
)
|
||||||
if (byokResult) {
|
if (byokResult) {
|
||||||
params[apiKeyParam] = byokResult.apiKey
|
params[apiKeyParam] = byokResult.apiKey
|
||||||
@@ -146,6 +146,12 @@ async function executeWithRetry<T>(
|
|||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from cost calculation */
|
||||||
|
interface ToolCostResult {
|
||||||
|
cost: number
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate cost based on pricing model
|
* Calculate cost based on pricing model
|
||||||
*/
|
*/
|
||||||
@@ -153,30 +159,25 @@ function calculateToolCost(
|
|||||||
pricing: ToolHostingPricing,
|
pricing: ToolHostingPricing,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
response: Record<string, unknown>
|
response: Record<string, unknown>
|
||||||
): number {
|
): ToolCostResult {
|
||||||
switch (pricing.type) {
|
switch (pricing.type) {
|
||||||
case 'per_request':
|
case 'per_request':
|
||||||
return pricing.cost
|
return { cost: pricing.cost }
|
||||||
|
|
||||||
case 'per_unit': {
|
|
||||||
const usage = pricing.getUsage(params, response)
|
|
||||||
return usage * pricing.costPerUnit
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'per_result': {
|
|
||||||
const resultCount = pricing.getResultCount(response)
|
|
||||||
const billableResults = pricing.maxResults
|
|
||||||
? Math.min(resultCount, pricing.maxResults)
|
|
||||||
: resultCount
|
|
||||||
return billableResults * pricing.costPerResult
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'per_second': {
|
case 'per_second': {
|
||||||
const duration = pricing.getDuration(response)
|
const duration = pricing.getDuration(response)
|
||||||
const billableDuration = pricing.minimumSeconds
|
const billableDuration = pricing.minimumSeconds
|
||||||
? Math.max(duration, pricing.minimumSeconds)
|
? Math.max(duration, pricing.minimumSeconds)
|
||||||
: duration
|
: duration
|
||||||
return billableDuration * pricing.costPerSecond
|
return { cost: billableDuration * pricing.costPerSecond }
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'custom': {
|
||||||
|
const result = pricing.getCost(params, response)
|
||||||
|
if (typeof result === 'number') {
|
||||||
|
return { cost: result }
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
@@ -200,7 +201,7 @@ async function logHostedToolUsage(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const cost = calculateToolCost(tool.hosting.pricing, params, response)
|
const { cost, metadata } = calculateToolCost(tool.hosting.pricing, params, response)
|
||||||
|
|
||||||
if (cost <= 0) return
|
if (cost <= 0) return
|
||||||
|
|
||||||
@@ -213,8 +214,9 @@ async function logHostedToolUsage(
|
|||||||
workspaceId: executionContext.workspaceId,
|
workspaceId: executionContext.workspaceId,
|
||||||
workflowId: executionContext.workflowId,
|
workflowId: executionContext.workflowId,
|
||||||
executionId: executionContext.executionId,
|
executionId: executionContext.executionId,
|
||||||
|
metadata,
|
||||||
})
|
})
|
||||||
logger.debug(`[${requestId}] Logged hosted tool usage for ${tool.id}: $${cost}`)
|
logger.debug(`[${requestId}] Logged hosted tool usage for ${tool.id}: $${cost}`, metadata ? { metadata } : {})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[${requestId}] Failed to log hosted tool usage for ${tool.id}:`, error)
|
logger.error(`[${requestId}] Failed to log hosted tool usage for ${tool.id}:`, error)
|
||||||
// Don't throw - usage logging should not break the main flow
|
// Don't throw - usage logging should not break the main flow
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
|
|||||||
description: 'Serper API Key',
|
description: 'Serper API Key',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hosting: {
|
|
||||||
envKeys: ['SERPER_API_KEY'],
|
|
||||||
apiKeyParam: 'apiKey',
|
|
||||||
byokProviderId: 'serper',
|
|
||||||
pricing: {
|
|
||||||
type: 'per_request',
|
|
||||||
cost: 0.001, // $0.001 per search (Serper pricing: ~$50/50k searches)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params) => `https://google.serper.dev/${params.type || 'search'}`,
|
url: (params) => `https://google.serper.dev/${params.type || 'search'}`,
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export interface ToolConfig<P = any, R = any> {
|
|||||||
* When configured, the tool can use Sim's hosted API keys if user doesn't provide their own.
|
* When configured, the tool can use Sim's hosted API keys if user doesn't provide their own.
|
||||||
* Usage is billed according to the pricing config.
|
* Usage is billed according to the pricing config.
|
||||||
*/
|
*/
|
||||||
hosting?: ToolHostingConfig
|
hosting?: ToolHostingConfig<P, R extends ToolResponse ? R : ToolResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableRow {
|
export interface TableRow {
|
||||||
@@ -188,28 +188,6 @@ export interface PerRequestPricing {
|
|||||||
cost: number
|
cost: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Usage-based on input/output size (e.g., LLM tokens, TTS characters) */
|
|
||||||
export interface PerUnitPricing {
|
|
||||||
type: 'per_unit'
|
|
||||||
/** Cost per unit in dollars */
|
|
||||||
costPerUnit: number
|
|
||||||
/** Unit of measurement */
|
|
||||||
unit: 'token' | 'character' | 'byte' | 'kb' | 'mb'
|
|
||||||
/** Extract usage count from params (before execution) or response (after execution) */
|
|
||||||
getUsage: (params: Record<string, unknown>, response?: Record<string, unknown>) => number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Based on result count (e.g., per search result, per email sent) */
|
|
||||||
export interface PerResultPricing {
|
|
||||||
type: 'per_result'
|
|
||||||
/** Cost per result in dollars */
|
|
||||||
costPerResult: number
|
|
||||||
/** Maximum results to bill for (cap) */
|
|
||||||
maxResults?: number
|
|
||||||
/** Extract result count from response */
|
|
||||||
getResultCount: (response: Record<string, unknown>) => number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Billed by execution duration (e.g., browser sessions, video processing) */
|
/** Billed by execution duration (e.g., browser sessions, video processing) */
|
||||||
export interface PerSecondPricing {
|
export interface PerSecondPricing {
|
||||||
type: 'per_second'
|
type: 'per_second'
|
||||||
@@ -221,14 +199,32 @@ export interface PerSecondPricing {
|
|||||||
getDuration: (response: Record<string, unknown>) => number
|
getDuration: (response: Record<string, unknown>) => number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from custom pricing calculation */
|
||||||
|
export interface CustomPricingResult {
|
||||||
|
/** Cost in dollars */
|
||||||
|
cost: number
|
||||||
|
/** Optional metadata about the cost calculation (e.g., breakdown from API) */
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Custom pricing calculated from params and response (e.g., Exa with different modes/result counts) */
|
||||||
|
export interface CustomPricing<P = Record<string, unknown>, R extends ToolResponse = ToolResponse> {
|
||||||
|
type: 'custom'
|
||||||
|
/** Calculate cost based on request params and response data. Returns cost or cost with metadata. */
|
||||||
|
getCost: (params: P, response: R['output']) => number | CustomPricingResult
|
||||||
|
}
|
||||||
|
|
||||||
/** Union of all pricing models */
|
/** Union of all pricing models */
|
||||||
export type ToolHostingPricing = PerRequestPricing | PerUnitPricing | PerResultPricing | PerSecondPricing
|
export type ToolHostingPricing<P = Record<string, unknown>, R extends ToolResponse = ToolResponse> =
|
||||||
|
| PerRequestPricing
|
||||||
|
| PerSecondPricing
|
||||||
|
| CustomPricing<P, R>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for hosted API key support
|
* Configuration for hosted API key support
|
||||||
* When configured, the tool can use Sim's hosted API keys if user doesn't provide their own
|
* When configured, the tool can use Sim's hosted API keys if user doesn't provide their own
|
||||||
*/
|
*/
|
||||||
export interface ToolHostingConfig {
|
export interface ToolHostingConfig<P = Record<string, unknown>, R extends ToolResponse = ToolResponse> {
|
||||||
/** Environment variable names to check for hosted keys (supports rotation with multiple keys) */
|
/** Environment variable names to check for hosted keys (supports rotation with multiple keys) */
|
||||||
envKeys: string[]
|
envKeys: string[]
|
||||||
/** The parameter name that receives the API key */
|
/** The parameter name that receives the API key */
|
||||||
@@ -236,5 +232,5 @@ export interface ToolHostingConfig {
|
|||||||
/** BYOK provider ID for workspace key lookup (e.g., 'serper') */
|
/** BYOK provider ID for workspace key lookup (e.g., 'serper') */
|
||||||
byokProviderId?: string
|
byokProviderId?: string
|
||||||
/** Pricing when using hosted key */
|
/** Pricing when using hosted key */
|
||||||
pricing: ToolHostingPricing
|
pricing: ToolHostingPricing<P, R>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user