mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-07 05:05:15 -05:00
* fix(azure): add azure-anthropic support to router, evaluator, copilot, and tokenization * added azure anthropic values to env * fix(azure): make anthropic-version configurable for azure-anthropic provider * fix(azure): thread provider credentials through guardrails and fix translate missing bedrockAccessKeyId * updated guardrails * ack'd PR comments * fix(azure): unify credential passing pattern across all LLM handlers - Pass all provider credentials unconditionally in router, evaluator (matching agent pattern) - Remove conditional if-branching on providerId for credential fields - Thread workspaceId through guardrails → hallucination validator for BYOK key resolution - Remove getApiKey() from hallucination validator, let executeProviderRequest handle it - Resolve vertex OAuth credentials in hallucination validator matching agent handler pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
338 lines
8.9 KiB
TypeScript
338 lines
8.9 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { AGENT_MODE_SYSTEM_PROMPT } from '@/lib/copilot/prompts'
|
|
import { getProviderDefaultModel } from '@/providers/models'
|
|
import type { ProviderId } from '@/providers/types'
|
|
|
|
const logger = createLogger('CopilotConfig')
|
|
|
|
/**
|
|
* Valid provider IDs for validation
|
|
*/
|
|
const VALID_PROVIDER_IDS: readonly ProviderId[] = [
|
|
'openai',
|
|
'azure-openai',
|
|
'anthropic',
|
|
'azure-anthropic',
|
|
'google',
|
|
'deepseek',
|
|
'xai',
|
|
'cerebras',
|
|
'mistral',
|
|
'groq',
|
|
'ollama',
|
|
] as const
|
|
|
|
/**
|
|
* Configuration validation constraints
|
|
*/
|
|
const VALIDATION_CONSTRAINTS = {
|
|
temperature: { min: 0, max: 2 },
|
|
maxTokens: { min: 1, max: 100000 },
|
|
maxSources: { min: 1, max: 20 },
|
|
similarityThreshold: { min: 0, max: 1 },
|
|
maxConversationHistory: { min: 1, max: 50 },
|
|
} as const
|
|
|
|
/**
|
|
* Copilot model types
|
|
*/
|
|
export type CopilotModelType = 'chat' | 'rag' | 'title'
|
|
|
|
/**
|
|
* Configuration validation result
|
|
*/
|
|
export interface ValidationResult {
|
|
isValid: boolean
|
|
errors: string[]
|
|
}
|
|
|
|
/**
|
|
* Copilot configuration interface
|
|
*/
|
|
export interface CopilotConfig {
|
|
// Chat LLM configuration
|
|
chat: {
|
|
defaultProvider: ProviderId
|
|
defaultModel: string
|
|
temperature: number
|
|
maxTokens: number
|
|
systemPrompt: string
|
|
}
|
|
// RAG (documentation search) LLM configuration
|
|
rag: {
|
|
defaultProvider: ProviderId
|
|
defaultModel: string
|
|
temperature: number
|
|
maxTokens: number
|
|
embeddingModel: string
|
|
maxSources: number
|
|
similarityThreshold: number
|
|
}
|
|
// General configuration
|
|
general: {
|
|
streamingEnabled: boolean
|
|
maxConversationHistory: number
|
|
titleGenerationModel: string
|
|
}
|
|
}
|
|
|
|
function validateProviderId(value: string | undefined): ProviderId | null {
|
|
if (!value) return null
|
|
return VALID_PROVIDER_IDS.includes(value as ProviderId) ? (value as ProviderId) : null
|
|
}
|
|
|
|
function parseFloatEnv(value: string | undefined, name: string): number | null {
|
|
if (!value) return null
|
|
const parsed = Number.parseFloat(value)
|
|
if (Number.isNaN(parsed)) {
|
|
logger.warn(`Invalid ${name}: ${value}. Expected a valid number.`)
|
|
return null
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function parseIntEnv(value: string | undefined, name: string): number | null {
|
|
if (!value) return null
|
|
const parsed = Number.parseInt(value, 10)
|
|
if (Number.isNaN(parsed)) {
|
|
logger.warn(`Invalid ${name}: ${value}. Expected a valid integer.`)
|
|
return null
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function parseBooleanEnv(value: string | undefined): boolean | null {
|
|
if (!value) return null
|
|
return value.toLowerCase() === 'true'
|
|
}
|
|
|
|
export const DEFAULT_COPILOT_CONFIG: CopilotConfig = {
|
|
chat: {
|
|
defaultProvider: 'anthropic',
|
|
defaultModel: 'claude-3-7-sonnet-latest',
|
|
temperature: 0.1,
|
|
maxTokens: 8192,
|
|
systemPrompt: AGENT_MODE_SYSTEM_PROMPT,
|
|
},
|
|
rag: {
|
|
defaultProvider: 'anthropic',
|
|
defaultModel: 'claude-3-7-sonnet-latest',
|
|
temperature: 0.1,
|
|
maxTokens: 2000,
|
|
embeddingModel: 'text-embedding-3-small',
|
|
maxSources: 10,
|
|
similarityThreshold: 0.3,
|
|
},
|
|
general: {
|
|
streamingEnabled: true,
|
|
maxConversationHistory: 10,
|
|
titleGenerationModel: 'claude-3-haiku-20240307',
|
|
},
|
|
}
|
|
|
|
function applyEnvironmentOverrides(config: CopilotConfig): void {
|
|
const chatProvider = validateProviderId(process.env.COPILOT_CHAT_PROVIDER)
|
|
if (chatProvider) {
|
|
config.chat.defaultProvider = chatProvider
|
|
} else if (process.env.COPILOT_CHAT_PROVIDER) {
|
|
logger.warn(
|
|
`Invalid COPILOT_CHAT_PROVIDER: ${process.env.COPILOT_CHAT_PROVIDER}. Valid providers: ${VALID_PROVIDER_IDS.join(', ')}`
|
|
)
|
|
}
|
|
|
|
if (process.env.COPILOT_CHAT_MODEL) {
|
|
config.chat.defaultModel = process.env.COPILOT_CHAT_MODEL
|
|
}
|
|
|
|
const chatTemperature = parseFloatEnv(
|
|
process.env.COPILOT_CHAT_TEMPERATURE,
|
|
'COPILOT_CHAT_TEMPERATURE'
|
|
)
|
|
if (chatTemperature !== null) {
|
|
config.chat.temperature = chatTemperature
|
|
}
|
|
|
|
const chatMaxTokens = parseIntEnv(process.env.COPILOT_CHAT_MAX_TOKENS, 'COPILOT_CHAT_MAX_TOKENS')
|
|
if (chatMaxTokens !== null) {
|
|
config.chat.maxTokens = chatMaxTokens
|
|
}
|
|
|
|
const ragProvider = validateProviderId(process.env.COPILOT_RAG_PROVIDER)
|
|
if (ragProvider) {
|
|
config.rag.defaultProvider = ragProvider
|
|
} else if (process.env.COPILOT_RAG_PROVIDER) {
|
|
logger.warn(
|
|
`Invalid COPILOT_RAG_PROVIDER: ${process.env.COPILOT_RAG_PROVIDER}. Valid providers: ${VALID_PROVIDER_IDS.join(', ')}`
|
|
)
|
|
}
|
|
|
|
if (process.env.COPILOT_RAG_MODEL) {
|
|
config.rag.defaultModel = process.env.COPILOT_RAG_MODEL
|
|
}
|
|
|
|
const ragTemperature = parseFloatEnv(
|
|
process.env.COPILOT_RAG_TEMPERATURE,
|
|
'COPILOT_RAG_TEMPERATURE'
|
|
)
|
|
if (ragTemperature !== null) {
|
|
config.rag.temperature = ragTemperature
|
|
}
|
|
|
|
const ragMaxTokens = parseIntEnv(process.env.COPILOT_RAG_MAX_TOKENS, 'COPILOT_RAG_MAX_TOKENS')
|
|
if (ragMaxTokens !== null) {
|
|
config.rag.maxTokens = ragMaxTokens
|
|
}
|
|
|
|
const ragMaxSources = parseIntEnv(process.env.COPILOT_RAG_MAX_SOURCES, 'COPILOT_RAG_MAX_SOURCES')
|
|
if (ragMaxSources !== null) {
|
|
config.rag.maxSources = ragMaxSources
|
|
}
|
|
|
|
const ragSimilarityThreshold = parseFloatEnv(
|
|
process.env.COPILOT_RAG_SIMILARITY_THRESHOLD,
|
|
'COPILOT_RAG_SIMILARITY_THRESHOLD'
|
|
)
|
|
if (ragSimilarityThreshold !== null) {
|
|
config.rag.similarityThreshold = ragSimilarityThreshold
|
|
}
|
|
|
|
const streamingEnabled = parseBooleanEnv(process.env.COPILOT_STREAMING_ENABLED)
|
|
if (streamingEnabled !== null) {
|
|
config.general.streamingEnabled = streamingEnabled
|
|
}
|
|
|
|
const maxConversationHistory = parseIntEnv(
|
|
process.env.COPILOT_MAX_CONVERSATION_HISTORY,
|
|
'COPILOT_MAX_CONVERSATION_HISTORY'
|
|
)
|
|
if (maxConversationHistory !== null) {
|
|
config.general.maxConversationHistory = maxConversationHistory
|
|
}
|
|
|
|
if (process.env.COPILOT_TITLE_GENERATION_MODEL) {
|
|
config.general.titleGenerationModel = process.env.COPILOT_TITLE_GENERATION_MODEL
|
|
}
|
|
}
|
|
|
|
export function getCopilotConfig(): CopilotConfig {
|
|
const config = structuredClone(DEFAULT_COPILOT_CONFIG)
|
|
|
|
try {
|
|
applyEnvironmentOverrides(config)
|
|
} catch (error) {
|
|
logger.warn('Error applying environment variable overrides, using defaults', { error })
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
export function getCopilotModel(type: CopilotModelType): {
|
|
provider: ProviderId
|
|
model: string
|
|
} {
|
|
const config = getCopilotConfig()
|
|
|
|
switch (type) {
|
|
case 'chat':
|
|
return {
|
|
provider: config.chat.defaultProvider,
|
|
model: config.chat.defaultModel,
|
|
}
|
|
case 'rag':
|
|
return {
|
|
provider: config.rag.defaultProvider,
|
|
model: config.rag.defaultModel,
|
|
}
|
|
case 'title':
|
|
return {
|
|
provider: config.chat.defaultProvider,
|
|
model: config.general.titleGenerationModel,
|
|
}
|
|
default:
|
|
throw new Error(`Unknown copilot model type: ${type}`)
|
|
}
|
|
}
|
|
|
|
function validateNumericValue(
|
|
value: number,
|
|
constraint: { min: number; max: number },
|
|
name: string
|
|
): string | null {
|
|
if (value < constraint.min || value > constraint.max) {
|
|
return `${name} must be between ${constraint.min} and ${constraint.max}`
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function validateCopilotConfig(config: CopilotConfig): ValidationResult {
|
|
const errors: string[] = []
|
|
|
|
try {
|
|
const chatDefaultModel = getProviderDefaultModel(config.chat.defaultProvider)
|
|
if (!chatDefaultModel) {
|
|
errors.push(`Chat provider '${config.chat.defaultProvider}' not found`)
|
|
}
|
|
} catch (error) {
|
|
errors.push(`Invalid chat provider: ${config.chat.defaultProvider}`)
|
|
}
|
|
|
|
try {
|
|
const ragDefaultModel = getProviderDefaultModel(config.rag.defaultProvider)
|
|
if (!ragDefaultModel) {
|
|
errors.push(`RAG provider '${config.rag.defaultProvider}' not found`)
|
|
}
|
|
} catch (error) {
|
|
errors.push(`Invalid RAG provider: ${config.rag.defaultProvider}`)
|
|
}
|
|
|
|
const validationChecks = [
|
|
{
|
|
value: config.chat.temperature,
|
|
constraint: VALIDATION_CONSTRAINTS.temperature,
|
|
name: 'Chat temperature',
|
|
},
|
|
{
|
|
value: config.rag.temperature,
|
|
constraint: VALIDATION_CONSTRAINTS.temperature,
|
|
name: 'RAG temperature',
|
|
},
|
|
{
|
|
value: config.chat.maxTokens,
|
|
constraint: VALIDATION_CONSTRAINTS.maxTokens,
|
|
name: 'Chat maxTokens',
|
|
},
|
|
{
|
|
value: config.rag.maxTokens,
|
|
constraint: VALIDATION_CONSTRAINTS.maxTokens,
|
|
name: 'RAG maxTokens',
|
|
},
|
|
{
|
|
value: config.rag.maxSources,
|
|
constraint: VALIDATION_CONSTRAINTS.maxSources,
|
|
name: 'RAG maxSources',
|
|
},
|
|
{
|
|
value: config.rag.similarityThreshold,
|
|
constraint: VALIDATION_CONSTRAINTS.similarityThreshold,
|
|
name: 'RAG similarityThreshold',
|
|
},
|
|
{
|
|
value: config.general.maxConversationHistory,
|
|
constraint: VALIDATION_CONSTRAINTS.maxConversationHistory,
|
|
name: 'General maxConversationHistory',
|
|
},
|
|
]
|
|
|
|
for (const check of validationChecks) {
|
|
const error = validateNumericValue(check.value, check.constraint, check.name)
|
|
if (error) {
|
|
errors.push(error)
|
|
}
|
|
}
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
}
|
|
}
|