mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { getApiKeyWithBYOK } from '@/lib/api-key/byok'
|
|
import { getCostMultiplier } from '@/lib/core/config/feature-flags'
|
|
import type { StreamingExecution } from '@/executor/types'
|
|
import { getProviderExecutor } from '@/providers/registry'
|
|
import type { ProviderId, ProviderRequest, ProviderResponse } from '@/providers/types'
|
|
import {
|
|
calculateCost,
|
|
generateStructuredOutputInstructions,
|
|
shouldBillModelUsage,
|
|
supportsReasoningEffort,
|
|
supportsTemperature,
|
|
supportsThinking,
|
|
supportsVerbosity,
|
|
} from '@/providers/utils'
|
|
|
|
const logger = createLogger('Providers')
|
|
|
|
/**
|
|
* Maximum number of iterations for tool call loops to prevent infinite loops.
|
|
* Used across all providers that support tool/function calling.
|
|
*/
|
|
export const MAX_TOOL_ITERATIONS = 20
|
|
|
|
function sanitizeRequest(request: ProviderRequest): ProviderRequest {
|
|
const sanitizedRequest = { ...request }
|
|
const model = sanitizedRequest.model
|
|
|
|
if (model && !supportsTemperature(model)) {
|
|
sanitizedRequest.temperature = undefined
|
|
}
|
|
|
|
if (model && !supportsReasoningEffort(model)) {
|
|
sanitizedRequest.reasoningEffort = undefined
|
|
}
|
|
|
|
if (model && !supportsVerbosity(model)) {
|
|
sanitizedRequest.verbosity = undefined
|
|
}
|
|
|
|
if (model && !supportsThinking(model)) {
|
|
sanitizedRequest.thinkingLevel = undefined
|
|
}
|
|
|
|
return sanitizedRequest
|
|
}
|
|
|
|
function isStreamingExecution(response: any): response is StreamingExecution {
|
|
return response && typeof response === 'object' && 'stream' in response && 'execution' in response
|
|
}
|
|
|
|
function isReadableStream(response: any): response is ReadableStream {
|
|
return response instanceof ReadableStream
|
|
}
|
|
|
|
export async function executeProviderRequest(
|
|
providerId: string,
|
|
request: ProviderRequest
|
|
): Promise<ProviderResponse | ReadableStream | StreamingExecution> {
|
|
const provider = await getProviderExecutor(providerId as ProviderId)
|
|
if (!provider) {
|
|
throw new Error(`Provider not found: ${providerId}`)
|
|
}
|
|
|
|
if (!provider.executeRequest) {
|
|
throw new Error(`Provider ${providerId} does not implement executeRequest`)
|
|
}
|
|
|
|
let resolvedRequest = sanitizeRequest(request)
|
|
let isBYOK = false
|
|
|
|
if (request.workspaceId) {
|
|
try {
|
|
const result = await getApiKeyWithBYOK(
|
|
providerId,
|
|
request.model,
|
|
request.workspaceId,
|
|
request.apiKey
|
|
)
|
|
resolvedRequest = { ...resolvedRequest, apiKey: result.apiKey }
|
|
isBYOK = result.isBYOK
|
|
} catch (error) {
|
|
logger.error('Failed to resolve API key:', {
|
|
provider: providerId,
|
|
model: request.model,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
resolvedRequest.isBYOK = isBYOK
|
|
const sanitizedRequest = resolvedRequest
|
|
|
|
if (sanitizedRequest.responseFormat) {
|
|
if (
|
|
typeof sanitizedRequest.responseFormat === 'string' &&
|
|
sanitizedRequest.responseFormat === ''
|
|
) {
|
|
logger.info('Empty response format provided, ignoring it')
|
|
sanitizedRequest.responseFormat = undefined
|
|
} else {
|
|
const structuredOutputInstructions = generateStructuredOutputInstructions(
|
|
sanitizedRequest.responseFormat
|
|
)
|
|
|
|
if (structuredOutputInstructions.trim()) {
|
|
const originalPrompt = sanitizedRequest.systemPrompt || ''
|
|
sanitizedRequest.systemPrompt =
|
|
`${originalPrompt}\n\n${structuredOutputInstructions}`.trim()
|
|
|
|
logger.info('Added structured output instructions to system prompt')
|
|
}
|
|
}
|
|
}
|
|
|
|
const response = await provider.executeRequest(sanitizedRequest)
|
|
|
|
if (isStreamingExecution(response)) {
|
|
logger.info('Provider returned StreamingExecution')
|
|
return response
|
|
}
|
|
|
|
if (isReadableStream(response)) {
|
|
logger.info('Provider returned ReadableStream')
|
|
return response
|
|
}
|
|
|
|
if (response.tokens) {
|
|
const { input: promptTokens = 0, output: completionTokens = 0 } = response.tokens
|
|
const useCachedInput = !!request.context && request.context.length > 0
|
|
|
|
const shouldBill = shouldBillModelUsage(response.model) && !isBYOK
|
|
if (shouldBill) {
|
|
const costMultiplier = getCostMultiplier()
|
|
response.cost = calculateCost(
|
|
response.model,
|
|
promptTokens,
|
|
completionTokens,
|
|
useCachedInput,
|
|
costMultiplier,
|
|
costMultiplier
|
|
)
|
|
} else {
|
|
response.cost = {
|
|
input: 0,
|
|
output: 0,
|
|
total: 0,
|
|
pricing: {
|
|
input: 0,
|
|
output: 0,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
}
|
|
if (isBYOK) {
|
|
logger.debug(`Not billing model usage for ${response.model} - workspace BYOK key used`)
|
|
} else {
|
|
logger.debug(
|
|
`Not billing model usage for ${response.model} - user provided API key or not hosted model`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return response
|
|
}
|