From 1edaf197b28b7aee84d5a83eb331d0fce8c2c4fb Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 6 Feb 2026 15:26:10 -0800 Subject: [PATCH] fix(azure): add azure-anthropic support to router, evaluator, copilot, and tokenization (#3158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --------- Co-authored-by: Claude Opus 4.6 --- apps/sim/app/api/copilot/chat/route.ts | 8 ++ apps/sim/app/api/guardrails/validate/route.ts | 33 ++++++++ apps/sim/blocks/blocks/agent.ts | 6 +- apps/sim/blocks/blocks/translate.ts | 3 +- apps/sim/blocks/utils.ts | 14 ++-- .../handlers/evaluator/evaluator-handler.ts | 23 ++---- .../handlers/router/router-handler.ts | 46 ++++------- apps/sim/lib/copilot/config.ts | 1 + apps/sim/lib/copilot/types.ts | 9 ++- apps/sim/lib/core/config/env.ts | 3 + .../lib/guardrails/validate_hallucination.ts | 77 +++++++++++++++---- apps/sim/lib/tokenization/constants.ts | 5 ++ apps/sim/lib/tokenization/estimators.ts | 1 + apps/sim/providers/azure-anthropic/index.ts | 4 +- apps/sim/tools/guardrails/validate.ts | 16 ++++ 15 files changed, 171 insertions(+), 78 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 9d31bf5c3..72c959d9a 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -285,6 +285,14 @@ export async function POST(req: NextRequest) { apiVersion: 'preview', endpoint: env.AZURE_OPENAI_ENDPOINT, } + } else if (providerEnv === 'azure-anthropic') { + providerConfig = { + provider: 'azure-anthropic', + model: envModel, + apiKey: env.AZURE_ANTHROPIC_API_KEY, + apiVersion: env.AZURE_ANTHROPIC_API_VERSION, + endpoint: env.AZURE_ANTHROPIC_ENDPOINT, + } } else if (providerEnv === 'vertex') { providerConfig = { provider: 'vertex', diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index 5f4738339..6e1b65750 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -23,7 +23,16 @@ export async function POST(request: NextRequest) { topK, model, apiKey, + azureEndpoint, + azureApiVersion, + vertexProject, + vertexLocation, + vertexCredential, + bedrockAccessKeyId, + bedrockSecretKey, + bedrockRegion, workflowId, + workspaceId, piiEntityTypes, piiMode, piiLanguage, @@ -110,7 +119,18 @@ export async function POST(request: NextRequest) { topK, model, apiKey, + { + azureEndpoint, + azureApiVersion, + vertexProject, + vertexLocation, + vertexCredential, + bedrockAccessKeyId, + bedrockSecretKey, + bedrockRegion, + }, workflowId, + workspaceId, piiEntityTypes, piiMode, piiLanguage, @@ -178,7 +198,18 @@ async function executeValidation( topK: string | undefined, model: string, apiKey: string | undefined, + providerCredentials: { + azureEndpoint?: string + azureApiVersion?: string + vertexProject?: string + vertexLocation?: string + vertexCredential?: string + bedrockAccessKeyId?: string + bedrockSecretKey?: string + bedrockRegion?: string + }, workflowId: string | undefined, + workspaceId: string | undefined, piiEntityTypes: string[] | undefined, piiMode: string | undefined, piiLanguage: string | undefined, @@ -219,7 +250,9 @@ async function executeValidation( topK: topK ? Number.parseInt(topK) : 10, // Default topK is 10 model: model, apiKey, + providerCredentials, workflowId, + workspaceId, requestId, }) } diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index 395efec87..bf8ec0d66 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -333,11 +333,11 @@ Return ONLY the JSON array.`, id: 'azureApiVersion', title: 'Azure API Version', type: 'short-input', - placeholder: '2024-07-01-preview', + placeholder: 'Enter API version', connectionDroppable: false, condition: { field: 'model', - value: providers['azure-openai'].models, + value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models], }, }, { @@ -715,7 +715,7 @@ Example 3 (Array Input): }, model: { type: 'string', description: 'AI model to use' }, apiKey: { type: 'string', description: 'Provider API key' }, - azureEndpoint: { type: 'string', description: 'Azure OpenAI endpoint URL' }, + azureEndpoint: { type: 'string', description: 'Azure endpoint URL' }, azureApiVersion: { type: 'string', description: 'Azure API version' }, vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' }, vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' }, diff --git a/apps/sim/blocks/blocks/translate.ts b/apps/sim/blocks/blocks/translate.ts index d0d647765..1385075c7 100644 --- a/apps/sim/blocks/blocks/translate.ts +++ b/apps/sim/blocks/blocks/translate.ts @@ -76,8 +76,9 @@ export const TranslateBlock: BlockConfig = { vertexProject: params.vertexProject, vertexLocation: params.vertexLocation, vertexCredential: params.vertexCredential, - bedrockRegion: params.bedrockRegion, + bedrockAccessKeyId: params.bedrockAccessKeyId, bedrockSecretKey: params.bedrockSecretKey, + bedrockRegion: params.bedrockRegion, }), }, }, diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 7de0b518a..eed4a5c37 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -80,7 +80,7 @@ export function getApiKeyCondition() { /** * Returns the standard provider credential subblocks used by LLM-based blocks. - * This includes: Vertex AI OAuth, API Key, Azure OpenAI, Vertex AI config, and Bedrock config. + * This includes: Vertex AI OAuth, API Key, Azure (OpenAI + Anthropic), Vertex AI config, and Bedrock config. * * Usage: Spread into your block's subBlocks array after block-specific fields */ @@ -111,25 +111,25 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { }, { id: 'azureEndpoint', - title: 'Azure OpenAI Endpoint', + title: 'Azure Endpoint', type: 'short-input', password: true, - placeholder: 'https://your-resource.openai.azure.com', + placeholder: 'https://your-resource.services.ai.azure.com', connectionDroppable: false, condition: { field: 'model', - value: providers['azure-openai'].models, + value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models], }, }, { id: 'azureApiVersion', title: 'Azure API Version', type: 'short-input', - placeholder: '2024-07-01-preview', + placeholder: 'Enter API version', connectionDroppable: false, condition: { field: 'model', - value: providers['azure-openai'].models, + value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models], }, }, { @@ -202,7 +202,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { */ export const PROVIDER_CREDENTIAL_INPUTS = { apiKey: { type: 'string', description: 'Provider API key' }, - azureEndpoint: { type: 'string', description: 'Azure OpenAI endpoint URL' }, + azureEndpoint: { type: 'string', description: 'Azure endpoint URL' }, azureApiVersion: { type: 'string', description: 'Azure API version' }, vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' }, vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' }, diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 3e95b2f85..8c432f1da 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -121,26 +121,17 @@ export class EvaluatorBlockHandler implements BlockHandler { temperature: EVALUATOR.DEFAULT_TEMPERATURE, apiKey: finalApiKey, + azureEndpoint: inputs.azureEndpoint, + azureApiVersion: inputs.azureApiVersion, + vertexProject: evaluatorConfig.vertexProject, + vertexLocation: evaluatorConfig.vertexLocation, + bedrockAccessKeyId: evaluatorConfig.bedrockAccessKeyId, + bedrockSecretKey: evaluatorConfig.bedrockSecretKey, + bedrockRegion: evaluatorConfig.bedrockRegion, workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, } - if (providerId === 'vertex') { - providerRequest.vertexProject = evaluatorConfig.vertexProject - providerRequest.vertexLocation = evaluatorConfig.vertexLocation - } - - if (providerId === 'azure-openai') { - providerRequest.azureEndpoint = inputs.azureEndpoint - providerRequest.azureApiVersion = inputs.azureApiVersion - } - - if (providerId === 'bedrock') { - providerRequest.bedrockAccessKeyId = evaluatorConfig.bedrockAccessKeyId - providerRequest.bedrockSecretKey = evaluatorConfig.bedrockSecretKey - providerRequest.bedrockRegion = evaluatorConfig.bedrockRegion - } - const response = await fetch(url.toString(), { method: 'POST', headers: await buildAuthHeaders(), diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 766a4aac6..541cdccca 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -96,26 +96,17 @@ export class RouterBlockHandler implements BlockHandler { context: JSON.stringify(messages), temperature: ROUTER.INFERENCE_TEMPERATURE, apiKey: finalApiKey, + azureEndpoint: inputs.azureEndpoint, + azureApiVersion: inputs.azureApiVersion, + vertexProject: routerConfig.vertexProject, + vertexLocation: routerConfig.vertexLocation, + bedrockAccessKeyId: routerConfig.bedrockAccessKeyId, + bedrockSecretKey: routerConfig.bedrockSecretKey, + bedrockRegion: routerConfig.bedrockRegion, workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, } - if (providerId === 'vertex') { - providerRequest.vertexProject = routerConfig.vertexProject - providerRequest.vertexLocation = routerConfig.vertexLocation - } - - if (providerId === 'azure-openai') { - providerRequest.azureEndpoint = inputs.azureEndpoint - providerRequest.azureApiVersion = inputs.azureApiVersion - } - - if (providerId === 'bedrock') { - providerRequest.bedrockAccessKeyId = routerConfig.bedrockAccessKeyId - providerRequest.bedrockSecretKey = routerConfig.bedrockSecretKey - providerRequest.bedrockRegion = routerConfig.bedrockRegion - } - const response = await fetch(url.toString(), { method: 'POST', headers: await buildAuthHeaders(), @@ -234,6 +225,13 @@ export class RouterBlockHandler implements BlockHandler { context: JSON.stringify(messages), temperature: ROUTER.INFERENCE_TEMPERATURE, apiKey: finalApiKey, + azureEndpoint: inputs.azureEndpoint, + azureApiVersion: inputs.azureApiVersion, + vertexProject: routerConfig.vertexProject, + vertexLocation: routerConfig.vertexLocation, + bedrockAccessKeyId: routerConfig.bedrockAccessKeyId, + bedrockSecretKey: routerConfig.bedrockSecretKey, + bedrockRegion: routerConfig.bedrockRegion, workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, responseFormat: { @@ -257,22 +255,6 @@ export class RouterBlockHandler implements BlockHandler { }, } - if (providerId === 'vertex') { - providerRequest.vertexProject = routerConfig.vertexProject - providerRequest.vertexLocation = routerConfig.vertexLocation - } - - if (providerId === 'azure-openai') { - providerRequest.azureEndpoint = inputs.azureEndpoint - providerRequest.azureApiVersion = inputs.azureApiVersion - } - - if (providerId === 'bedrock') { - providerRequest.bedrockAccessKeyId = routerConfig.bedrockAccessKeyId - providerRequest.bedrockSecretKey = routerConfig.bedrockSecretKey - providerRequest.bedrockRegion = routerConfig.bedrockRegion - } - const response = await fetch(url.toString(), { method: 'POST', headers: await buildAuthHeaders(), diff --git a/apps/sim/lib/copilot/config.ts b/apps/sim/lib/copilot/config.ts index 4b9c89274..5700e9930 100644 --- a/apps/sim/lib/copilot/config.ts +++ b/apps/sim/lib/copilot/config.ts @@ -12,6 +12,7 @@ const VALID_PROVIDER_IDS: readonly ProviderId[] = [ 'openai', 'azure-openai', 'anthropic', + 'azure-anthropic', 'google', 'deepseek', 'xai', diff --git a/apps/sim/lib/copilot/types.ts b/apps/sim/lib/copilot/types.ts index 6ed813308..68e097039 100644 --- a/apps/sim/lib/copilot/types.ts +++ b/apps/sim/lib/copilot/types.ts @@ -147,6 +147,13 @@ export type CopilotProviderConfig = apiVersion?: string endpoint?: string } + | { + provider: 'azure-anthropic' + model: string + apiKey?: string + apiVersion?: string + endpoint?: string + } | { provider: 'vertex' model: string @@ -155,7 +162,7 @@ export type CopilotProviderConfig = vertexLocation?: string } | { - provider: Exclude + provider: Exclude model?: string apiKey?: string } diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 8440de3bc..48e1a630d 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -95,6 +95,9 @@ export const env = createEnv({ AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Shared Azure OpenAI service endpoint AZURE_OPENAI_API_VERSION: z.string().optional(), // Shared Azure OpenAI API version AZURE_OPENAI_API_KEY: z.string().min(1).optional(), // Shared Azure OpenAI API key + AZURE_ANTHROPIC_ENDPOINT: z.string().url().optional(), // Azure Anthropic service endpoint + AZURE_ANTHROPIC_API_KEY: z.string().min(1).optional(), // Azure Anthropic API key + AZURE_ANTHROPIC_API_VERSION: z.string().min(1).optional(), // Azure Anthropic API version (e.g. 2023-06-01) KB_OPENAI_MODEL_NAME: z.string().optional(), // Knowledge base OpenAI model name (works with both regular OpenAI and Azure OpenAI) WAND_OPENAI_MODEL_NAME: z.string().optional(), // Wand generation OpenAI model name (works with both regular OpenAI and Azure OpenAI) OCR_AZURE_ENDPOINT: z.string().url().optional(), // Azure Mistral OCR service endpoint diff --git a/apps/sim/lib/guardrails/validate_hallucination.ts b/apps/sim/lib/guardrails/validate_hallucination.ts index b2668f248..48a91fb81 100644 --- a/apps/sim/lib/guardrails/validate_hallucination.ts +++ b/apps/sim/lib/guardrails/validate_hallucination.ts @@ -1,7 +1,11 @@ +import { db } from '@sim/db' +import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' import { getBaseUrl } from '@/lib/core/utils/urls' +import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { executeProviderRequest } from '@/providers' -import { getApiKey, getProviderFromModel } from '@/providers/utils' +import { getProviderFromModel } from '@/providers/utils' const logger = createLogger('HallucinationValidator') @@ -19,7 +23,18 @@ export interface HallucinationValidationInput { topK: number // Number of chunks to retrieve, default 10 model: string apiKey?: string + providerCredentials?: { + azureEndpoint?: string + azureApiVersion?: string + vertexProject?: string + vertexLocation?: string + vertexCredential?: string + bedrockAccessKeyId?: string + bedrockSecretKey?: string + bedrockRegion?: string + } workflowId?: string + workspaceId?: string requestId: string } @@ -89,7 +104,9 @@ async function scoreHallucinationWithLLM( userInput: string, ragContext: string[], model: string, - apiKey: string, + apiKey: string | undefined, + providerCredentials: HallucinationValidationInput['providerCredentials'], + workspaceId: string | undefined, requestId: string ): Promise<{ score: number; reasoning: string }> { try { @@ -127,6 +144,23 @@ Evaluate the consistency and provide your score and reasoning in JSON format.` const providerId = getProviderFromModel(model) + let finalApiKey: string | undefined = apiKey + if (providerId === 'vertex' && providerCredentials?.vertexCredential) { + const credential = await db.query.account.findFirst({ + where: eq(account.id, providerCredentials.vertexCredential), + }) + if (credential) { + const { accessToken } = await refreshTokenIfNeeded( + requestId, + credential, + providerCredentials.vertexCredential + ) + if (accessToken) { + finalApiKey = accessToken + } + } + } + const response = await executeProviderRequest(providerId, { model, systemPrompt, @@ -137,7 +171,15 @@ Evaluate the consistency and provide your score and reasoning in JSON format.` }, ], temperature: 0.1, // Low temperature for consistent scoring - apiKey, + apiKey: finalApiKey, + azureEndpoint: providerCredentials?.azureEndpoint, + azureApiVersion: providerCredentials?.azureApiVersion, + vertexProject: providerCredentials?.vertexProject, + vertexLocation: providerCredentials?.vertexLocation, + bedrockAccessKeyId: providerCredentials?.bedrockAccessKeyId, + bedrockSecretKey: providerCredentials?.bedrockSecretKey, + bedrockRegion: providerCredentials?.bedrockRegion, + workspaceId, }) if (response instanceof ReadableStream || ('stream' in response && 'execution' in response)) { @@ -184,8 +226,18 @@ Evaluate the consistency and provide your score and reasoning in JSON format.` export async function validateHallucination( input: HallucinationValidationInput ): Promise { - const { userInput, knowledgeBaseId, threshold, topK, model, apiKey, workflowId, requestId } = - input + const { + userInput, + knowledgeBaseId, + threshold, + topK, + model, + apiKey, + providerCredentials, + workflowId, + workspaceId, + requestId, + } = input try { if (!userInput || userInput.trim().length === 0) { @@ -202,17 +254,6 @@ export async function validateHallucination( } } - let finalApiKey: string - try { - const providerId = getProviderFromModel(model) - finalApiKey = getApiKey(providerId, model, apiKey) - } catch (error: any) { - return { - passed: false, - error: `API key error: ${error.message}`, - } - } - // Step 1: Query knowledge base with RAG const ragContext = await queryKnowledgeBase( knowledgeBaseId, @@ -234,7 +275,9 @@ export async function validateHallucination( userInput, ragContext, model, - finalApiKey, + apiKey, + providerCredentials, + workspaceId, requestId ) diff --git a/apps/sim/lib/tokenization/constants.ts b/apps/sim/lib/tokenization/constants.ts index 010ef4743..a10b1995d 100644 --- a/apps/sim/lib/tokenization/constants.ts +++ b/apps/sim/lib/tokenization/constants.ts @@ -21,6 +21,11 @@ export const TOKENIZATION_CONFIG = { confidence: 'high', supportedMethods: ['heuristic', 'fallback'], }, + 'azure-anthropic': { + avgCharsPerToken: 4.5, + confidence: 'high', + supportedMethods: ['heuristic', 'fallback'], + }, google: { avgCharsPerToken: 5, confidence: 'medium', diff --git a/apps/sim/lib/tokenization/estimators.ts b/apps/sim/lib/tokenization/estimators.ts index 53ce71965..01aed1c1e 100644 --- a/apps/sim/lib/tokenization/estimators.ts +++ b/apps/sim/lib/tokenization/estimators.ts @@ -204,6 +204,7 @@ export function estimateTokenCount(text: string, providerId?: string): TokenEsti estimatedTokens = estimateOpenAITokens(text) break case 'anthropic': + case 'azure-anthropic': estimatedTokens = estimateAnthropicTokens(text) break case 'google': diff --git a/apps/sim/providers/azure-anthropic/index.ts b/apps/sim/providers/azure-anthropic/index.ts index efb131be1..721e36339 100644 --- a/apps/sim/providers/azure-anthropic/index.ts +++ b/apps/sim/providers/azure-anthropic/index.ts @@ -35,6 +35,8 @@ export const azureAnthropicProvider: ProviderConfig = { // The SDK appends /v1/messages automatically const baseURL = `${request.azureEndpoint.replace(/\/$/, '')}/anthropic` + const anthropicVersion = request.azureApiVersion || '2023-06-01' + return executeAnthropicProviderRequest( { ...request, @@ -49,7 +51,7 @@ export const azureAnthropicProvider: ProviderConfig = { apiKey, defaultHeaders: { 'api-key': apiKey, - 'anthropic-version': '2023-06-01', + 'anthropic-version': anthropicVersion, ...(useNativeStructuredOutputs ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } : {}), diff --git a/apps/sim/tools/guardrails/validate.ts b/apps/sim/tools/guardrails/validate.ts index f791fa89c..124795c44 100644 --- a/apps/sim/tools/guardrails/validate.ts +++ b/apps/sim/tools/guardrails/validate.ts @@ -9,6 +9,14 @@ export interface GuardrailsValidateInput { topK?: string model?: string apiKey?: string + azureEndpoint?: string + azureApiVersion?: string + vertexProject?: string + vertexLocation?: string + vertexCredential?: string + bedrockAccessKeyId?: string + bedrockSecretKey?: string + bedrockRegion?: string piiEntityTypes?: string[] piiMode?: string piiLanguage?: string @@ -166,6 +174,14 @@ export const guardrailsValidateTool: ToolConfig