mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-13 07:55:09 -05:00
Compare commits
3 Commits
main
...
feat/sim-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5c8aec07d | ||
|
|
3e6527a540 | ||
|
|
2cdb89681b |
@@ -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'] as const
|
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'serper'] as const
|
||||||
|
|
||||||
const UpsertKeySchema = z.object({
|
const UpsertKeySchema = z.object({
|
||||||
providerId: z.enum(VALID_PROVIDERS),
|
providerId: z.enum(VALID_PROVIDERS),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
buildCanonicalIndex,
|
buildCanonicalIndex,
|
||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
isSubBlockFeatureEnabled,
|
isSubBlockFeatureEnabled,
|
||||||
|
isSubBlockHiddenByHostedKey,
|
||||||
isSubBlockVisibleForMode,
|
isSubBlockVisibleForMode,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
|
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
|
||||||
@@ -108,6 +109,9 @@ export function useEditorSubblockLayout(
|
|||||||
// Check required feature if specified - declarative feature gating
|
// Check required feature if specified - declarative feature gating
|
||||||
if (!isSubBlockFeatureEnabled(block)) return false
|
if (!isSubBlockFeatureEnabled(block)) return false
|
||||||
|
|
||||||
|
// Hide tool API key fields when hosted key is available
|
||||||
|
if (isSubBlockHiddenByHostedKey(block)) return false
|
||||||
|
|
||||||
// Special handling for trigger-config type (legacy trigger configuration UI)
|
// Special handling for trigger-config type (legacy trigger configuration UI)
|
||||||
if (block.type === ('trigger-config' as SubBlockType)) {
|
if (block.type === ('trigger-config' as SubBlockType)) {
|
||||||
const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers'
|
const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
evaluateSubBlockCondition,
|
evaluateSubBlockCondition,
|
||||||
hasAdvancedValues,
|
hasAdvancedValues,
|
||||||
isSubBlockFeatureEnabled,
|
isSubBlockFeatureEnabled,
|
||||||
|
isSubBlockHiddenByHostedKey,
|
||||||
isSubBlockVisibleForMode,
|
isSubBlockVisibleForMode,
|
||||||
resolveDependencyValue,
|
resolveDependencyValue,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
@@ -828,6 +829,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
|||||||
if (block.hidden) return false
|
if (block.hidden) return false
|
||||||
if (block.hideFromPreview) return false
|
if (block.hideFromPreview) return false
|
||||||
if (!isSubBlockFeatureEnabled(block)) return false
|
if (!isSubBlockFeatureEnabled(block)) return false
|
||||||
|
if (isSubBlockHiddenByHostedKey(block)) return false
|
||||||
|
|
||||||
const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers'
|
const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers'
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
import { AnthropicIcon, GeminiIcon, MistralIcon, OpenAIIcon } from '@/components/icons'
|
import { AnthropicIcon, GeminiIcon, MistralIcon, OpenAIIcon, SerperIcon } from '@/components/icons'
|
||||||
import { Skeleton } from '@/components/ui'
|
import { Skeleton } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
type BYOKKey,
|
type BYOKKey,
|
||||||
@@ -60,6 +60,13 @@ const PROVIDERS: {
|
|||||||
description: 'LLM calls and Knowledge Base OCR',
|
description: 'LLM calls and Knowledge Base OCR',
|
||||||
placeholder: 'Enter your API key',
|
placeholder: 'Enter your API key',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'serper',
|
||||||
|
name: 'Serper',
|
||||||
|
icon: SerperIcon,
|
||||||
|
description: 'Web search tool',
|
||||||
|
placeholder: 'Enter your Serper API key',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function BYOKKeySkeleton() {
|
function BYOKKeySkeleton() {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ 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: {
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ export interface SubBlockConfig {
|
|||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
hideFromPreview?: boolean // Hide this subblock from the workflow block preview
|
hideFromPreview?: boolean // Hide this subblock from the workflow block preview
|
||||||
requiresFeature?: string // Environment variable name that must be truthy for this subblock to be visible
|
requiresFeature?: string // Environment variable name that must be truthy for this subblock to be visible
|
||||||
|
hideWhenHosted?: boolean // Hide this subblock when running on hosted sim
|
||||||
description?: string
|
description?: string
|
||||||
tooltip?: string // Tooltip text displayed via info icon next to the title
|
tooltip?: string // Tooltip text displayed via info icon next to the title
|
||||||
value?: (params: Record<string, any>) => string
|
value?: (params: Record<string, any>) => string
|
||||||
|
|||||||
@@ -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'
|
export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'serper'
|
||||||
|
|
||||||
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'
|
export type BYOKProviderId = 'openai' | 'anthropic' | 'google' | 'mistral' | 'serper'
|
||||||
|
|
||||||
export interface BYOKKeyResult {
|
export interface BYOKKeyResult {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
|
|
||||||
export type CanonicalMode = 'basic' | 'advanced'
|
export type CanonicalMode = 'basic' | 'advanced'
|
||||||
@@ -270,3 +271,12 @@ export function isSubBlockFeatureEnabled(subBlock: SubBlockConfig): boolean {
|
|||||||
if (!subBlock.requiresFeature) return true
|
if (!subBlock.requiresFeature) return true
|
||||||
return isTruthy(getEnv(subBlock.requiresFeature))
|
return isTruthy(getEnv(subBlock.requiresFeature))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a subblock should be hidden because we're running on hosted Sim.
|
||||||
|
* Used for tool API key fields that should be hidden when Sim provides hosted keys.
|
||||||
|
*/
|
||||||
|
export function isSubBlockHiddenByHostedKey(subBlock: SubBlockConfig): boolean {
|
||||||
|
if (!subBlock.hideWhenHosted) return false
|
||||||
|
return isHosted
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
isCanonicalPair,
|
isCanonicalPair,
|
||||||
isNonEmptyValue,
|
isNonEmptyValue,
|
||||||
isSubBlockFeatureEnabled,
|
isSubBlockFeatureEnabled,
|
||||||
|
isSubBlockHiddenByHostedKey,
|
||||||
resolveCanonicalMode,
|
resolveCanonicalMode,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
@@ -49,6 +50,7 @@ function shouldSerializeSubBlock(
|
|||||||
canonicalModeOverrides?: CanonicalModeOverrides
|
canonicalModeOverrides?: CanonicalModeOverrides
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!isSubBlockFeatureEnabled(subBlockConfig)) return false
|
if (!isSubBlockFeatureEnabled(subBlockConfig)) return false
|
||||||
|
if (isSubBlockHiddenByHostedKey(subBlockConfig)) return false
|
||||||
|
|
||||||
if (subBlockConfig.mode === 'trigger') {
|
if (subBlockConfig.mode === 'trigger') {
|
||||||
if (!isTriggerContext && !isTriggerCategory) return false
|
if (!isTriggerContext && !isTriggerCategory) return false
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { generateInternalToken } from '@/lib/auth/internal'
|
import { generateInternalToken } from '@/lib/auth/internal'
|
||||||
|
import { getBYOKKey } from '@/lib/api-key/byok'
|
||||||
|
import { logFixedUsage } from '@/lib/billing/core/usage-log'
|
||||||
|
import { env } from '@/lib/core/config/env'
|
||||||
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
||||||
import {
|
import {
|
||||||
secureFetchWithPinnedIP,
|
secureFetchWithPinnedIP,
|
||||||
@@ -13,7 +17,12 @@ import { resolveSkillContent } from '@/executor/handlers/agent/skills-resolver'
|
|||||||
import type { ExecutionContext } from '@/executor/types'
|
import type { ExecutionContext } from '@/executor/types'
|
||||||
import type { ErrorInfo } from '@/tools/error-extractors'
|
import type { ErrorInfo } from '@/tools/error-extractors'
|
||||||
import { extractErrorMessage } from '@/tools/error-extractors'
|
import { extractErrorMessage } from '@/tools/error-extractors'
|
||||||
import type { OAuthTokenPayload, ToolConfig, ToolResponse } from '@/tools/types'
|
import type {
|
||||||
|
OAuthTokenPayload,
|
||||||
|
ToolConfig,
|
||||||
|
ToolHostingPricing,
|
||||||
|
ToolResponse,
|
||||||
|
} from '@/tools/types'
|
||||||
import {
|
import {
|
||||||
formatRequestParams,
|
formatRequestParams,
|
||||||
getTool,
|
getTool,
|
||||||
@@ -23,6 +32,195 @@ import {
|
|||||||
|
|
||||||
const logger = createLogger('Tools')
|
const logger = createLogger('Tools')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a hosted API key from environment variables
|
||||||
|
* Supports rotation when multiple keys are configured
|
||||||
|
*/
|
||||||
|
function getHostedKeyFromEnv(envKeys: string[]): string | null {
|
||||||
|
const keys = envKeys
|
||||||
|
.map((key) => env[key as keyof typeof env])
|
||||||
|
.filter((value): value is string => Boolean(value))
|
||||||
|
|
||||||
|
if (keys.length === 0) return null
|
||||||
|
|
||||||
|
// Round-robin rotation based on current minute
|
||||||
|
const currentMinute = Math.floor(Date.now() / 60000)
|
||||||
|
const keyIndex = currentMinute % keys.length
|
||||||
|
|
||||||
|
return keys[keyIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject hosted API key if tool supports it and user didn't provide one.
|
||||||
|
* Checks BYOK workspace keys first, then falls back to hosted env keys.
|
||||||
|
* Returns whether a hosted (billable) key was injected.
|
||||||
|
*/
|
||||||
|
async function injectHostedKeyIfNeeded(
|
||||||
|
tool: ToolConfig,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
executionContext: ExecutionContext | undefined,
|
||||||
|
requestId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!tool.hosting) return false
|
||||||
|
if (!isHosted) return false
|
||||||
|
|
||||||
|
const { envKeys, apiKeyParam, byokProviderId } = tool.hosting
|
||||||
|
const userProvidedKey = params[apiKeyParam]
|
||||||
|
|
||||||
|
if (userProvidedKey) {
|
||||||
|
logger.debug(`[${requestId}] User provided API key for ${tool.id}, skipping hosted key`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check BYOK workspace key first
|
||||||
|
if (byokProviderId && executionContext?.workspaceId) {
|
||||||
|
try {
|
||||||
|
const byokResult = await getBYOKKey(
|
||||||
|
executionContext.workspaceId,
|
||||||
|
byokProviderId as 'openai' | 'anthropic' | 'google' | 'mistral' | 'serper'
|
||||||
|
)
|
||||||
|
if (byokResult) {
|
||||||
|
params[apiKeyParam] = byokResult.apiKey
|
||||||
|
logger.info(`[${requestId}] Using BYOK key for ${tool.id}`)
|
||||||
|
return false // Don't bill - user's own key
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${requestId}] Failed to get BYOK key for ${tool.id}:`, error)
|
||||||
|
// Fall through to hosted key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to hosted env key
|
||||||
|
const hostedKey = getHostedKeyFromEnv(envKeys)
|
||||||
|
if (!hostedKey) {
|
||||||
|
logger.debug(`[${requestId}] No hosted key available for ${tool.id}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
params[apiKeyParam] = hostedKey
|
||||||
|
logger.info(`[${requestId}] Using hosted key for ${tool.id}`)
|
||||||
|
return true // Bill the user
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is a rate limit (throttling) error
|
||||||
|
*/
|
||||||
|
function isRateLimitError(error: unknown): boolean {
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
const status = (error as { status?: number }).status
|
||||||
|
// 429 = Too Many Requests, 503 = Service Unavailable (sometimes used for rate limiting)
|
||||||
|
if (status === 429 || status === 503) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function with exponential backoff retry for rate limiting errors.
|
||||||
|
* Only used for hosted key requests.
|
||||||
|
*/
|
||||||
|
async function executeWithRetry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
requestId: string,
|
||||||
|
toolId: string,
|
||||||
|
maxRetries = 3,
|
||||||
|
baseDelayMs = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: unknown
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
|
||||||
|
if (!isRateLimitError(error) || attempt === maxRetries) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayMs = baseDelayMs * Math.pow(2, attempt)
|
||||||
|
logger.warn(`[${requestId}] Rate limited for ${toolId}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate cost based on pricing model
|
||||||
|
*/
|
||||||
|
function calculateToolCost(
|
||||||
|
pricing: ToolHostingPricing,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
response: Record<string, unknown>
|
||||||
|
): number {
|
||||||
|
switch (pricing.type) {
|
||||||
|
case 'per_request':
|
||||||
|
return 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': {
|
||||||
|
const duration = pricing.getDuration(response)
|
||||||
|
const billableDuration = pricing.minimumSeconds
|
||||||
|
? Math.max(duration, pricing.minimumSeconds)
|
||||||
|
: duration
|
||||||
|
return billableDuration * pricing.costPerSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
const exhaustiveCheck: never = pricing
|
||||||
|
throw new Error(`Unknown pricing type: ${(exhaustiveCheck as ToolHostingPricing).type}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log usage for a tool that used a hosted API key
|
||||||
|
*/
|
||||||
|
async function logHostedToolUsage(
|
||||||
|
tool: ToolConfig,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
response: Record<string, unknown>,
|
||||||
|
executionContext: ExecutionContext | undefined,
|
||||||
|
requestId: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (!tool.hosting?.pricing || !executionContext?.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cost = calculateToolCost(tool.hosting.pricing, params, response)
|
||||||
|
|
||||||
|
if (cost <= 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await logFixedUsage({
|
||||||
|
userId: executionContext.userId,
|
||||||
|
source: 'workflow',
|
||||||
|
description: `tool:${tool.id}`,
|
||||||
|
cost,
|
||||||
|
workspaceId: executionContext.workspaceId,
|
||||||
|
workflowId: executionContext.workflowId,
|
||||||
|
executionId: executionContext.executionId,
|
||||||
|
})
|
||||||
|
logger.debug(`[${requestId}] Logged hosted tool usage for ${tool.id}: $${cost}`)
|
||||||
|
} catch (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes a tool ID by stripping resource ID suffix (UUID).
|
* Normalizes a tool ID by stripping resource ID suffix (UUID).
|
||||||
* Workflow tools: 'workflow_executor_<uuid>' -> 'workflow_executor'
|
* Workflow tools: 'workflow_executor_<uuid>' -> 'workflow_executor'
|
||||||
@@ -279,6 +477,14 @@ export async function executeTool(
|
|||||||
throw new Error(`Tool not found: ${toolId}`)
|
throw new Error(`Tool not found: ${toolId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject hosted API key if tool supports it and user didn't provide one
|
||||||
|
const isUsingHostedKey = await injectHostedKeyIfNeeded(
|
||||||
|
tool,
|
||||||
|
contextParams,
|
||||||
|
executionContext,
|
||||||
|
requestId
|
||||||
|
)
|
||||||
|
|
||||||
// If we have a credential parameter, fetch the access token
|
// If we have a credential parameter, fetch the access token
|
||||||
if (contextParams.credential) {
|
if (contextParams.credential) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -387,6 +593,11 @@ export async function executeTool(
|
|||||||
// Process file outputs if execution context is available
|
// Process file outputs if execution context is available
|
||||||
finalResult = await processFileOutputs(finalResult, tool, executionContext)
|
finalResult = await processFileOutputs(finalResult, tool, executionContext)
|
||||||
|
|
||||||
|
// Log usage for hosted key if execution was successful
|
||||||
|
if (isUsingHostedKey && finalResult.success) {
|
||||||
|
await logHostedToolUsage(tool, contextParams, finalResult.output, executionContext, requestId)
|
||||||
|
}
|
||||||
|
|
||||||
// Add timing data to the result
|
// Add timing data to the result
|
||||||
const endTime = new Date()
|
const endTime = new Date()
|
||||||
const endTimeISO = endTime.toISOString()
|
const endTimeISO = endTime.toISOString()
|
||||||
@@ -402,7 +613,14 @@ export async function executeTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
|
// Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
|
||||||
const result = await executeToolRequest(toolId, tool, contextParams)
|
// Wrap with retry logic for hosted keys to handle rate limiting due to higher usage
|
||||||
|
const result = isUsingHostedKey
|
||||||
|
? await executeWithRetry(
|
||||||
|
() => executeToolRequest(toolId, tool, contextParams),
|
||||||
|
requestId,
|
||||||
|
toolId
|
||||||
|
)
|
||||||
|
: await executeToolRequest(toolId, tool, contextParams)
|
||||||
|
|
||||||
// Apply post-processing if available and not skipped
|
// Apply post-processing if available and not skipped
|
||||||
let finalResult = result
|
let finalResult = result
|
||||||
@@ -420,6 +638,11 @@ export async function executeTool(
|
|||||||
// Process file outputs if execution context is available
|
// Process file outputs if execution context is available
|
||||||
finalResult = await processFileOutputs(finalResult, tool, executionContext)
|
finalResult = await processFileOutputs(finalResult, tool, executionContext)
|
||||||
|
|
||||||
|
// Log usage for hosted key if execution was successful
|
||||||
|
if (isUsingHostedKey && finalResult.success) {
|
||||||
|
await logHostedToolUsage(tool, contextParams, finalResult.output, executionContext, requestId)
|
||||||
|
}
|
||||||
|
|
||||||
// Add timing data to the result
|
// Add timing data to the result
|
||||||
const endTime = new Date()
|
const endTime = new Date()
|
||||||
const endTimeISO = endTime.toISOString()
|
const endTimeISO = endTime.toISOString()
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ 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'}`,
|
||||||
|
|||||||
@@ -127,6 +127,13 @@ export interface ToolConfig<P = any, R = any> {
|
|||||||
* Maps param IDs to their enrichment configuration.
|
* Maps param IDs to their enrichment configuration.
|
||||||
*/
|
*/
|
||||||
schemaEnrichment?: Record<string, SchemaEnrichmentConfig>
|
schemaEnrichment?: Record<string, SchemaEnrichmentConfig>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hosted API key configuration for this tool.
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
hosting?: ToolHostingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableRow {
|
export interface TableRow {
|
||||||
@@ -170,3 +177,64 @@ export interface SchemaEnrichmentConfig {
|
|||||||
required?: string[]
|
required?: string[]
|
||||||
} | null>
|
} | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pricing models for hosted API key usage
|
||||||
|
*/
|
||||||
|
/** Flat fee per API call (e.g., Serper search) */
|
||||||
|
export interface PerRequestPricing {
|
||||||
|
type: 'per_request'
|
||||||
|
/** Cost per request in dollars */
|
||||||
|
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) */
|
||||||
|
export interface PerSecondPricing {
|
||||||
|
type: 'per_second'
|
||||||
|
/** Cost per second in dollars */
|
||||||
|
costPerSecond: number
|
||||||
|
/** Minimum billable seconds */
|
||||||
|
minimumSeconds?: number
|
||||||
|
/** Extract duration from response (in seconds) */
|
||||||
|
getDuration: (response: Record<string, unknown>) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Union of all pricing models */
|
||||||
|
export type ToolHostingPricing = PerRequestPricing | PerUnitPricing | PerResultPricing | PerSecondPricing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for hosted API key support
|
||||||
|
* When configured, the tool can use Sim's hosted API keys if user doesn't provide their own
|
||||||
|
*/
|
||||||
|
export interface ToolHostingConfig {
|
||||||
|
/** Environment variable names to check for hosted keys (supports rotation with multiple keys) */
|
||||||
|
envKeys: string[]
|
||||||
|
/** The parameter name that receives the API key */
|
||||||
|
apiKeyParam: string
|
||||||
|
/** BYOK provider ID for workspace key lookup (e.g., 'serper') */
|
||||||
|
byokProviderId?: string
|
||||||
|
/** Pricing when using hosted key */
|
||||||
|
pricing: ToolHostingPricing
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user