Files
sim/apps/sim/lib/copilot/config.ts
Waleed 1edaf197b2 fix(azure): add azure-anthropic support to router, evaluator, copilot, and tokenization (#3158)
* 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>
2026-02-06 15:26:10 -08:00

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,
}
}