diff --git a/src/LLMProviders/embeddingManager.ts b/src/LLMProviders/embeddingManager.ts index c46030925..453133559 100644 --- a/src/LLMProviders/embeddingManager.ts +++ b/src/LLMProviders/embeddingManager.ts @@ -9,11 +9,12 @@ import { CohereEmbeddings } from "@langchain/cohere"; import { Embeddings } from "@langchain/core/embeddings"; import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai"; import { OllamaEmbeddings } from "@langchain/ollama"; -import { AzureOpenAIEmbeddings, OpenAIEmbeddings } from "@langchain/openai"; +import { OpenAIEmbeddings } from "@langchain/openai"; import { Notice } from "obsidian"; import { BrevilabsClient } from "./brevilabsClient"; import { CustomJinaEmbeddings } from "./CustomJinaEmbeddings"; import { CustomOpenAIEmbeddings } from "./CustomOpenAIEmbeddings"; +import { normalizeAzureUrl } from "./chatModelManager"; type EmbeddingConstructorType = new (config: any) => Embeddings; @@ -23,7 +24,7 @@ const EMBEDDING_PROVIDER_CONSTRUCTORS = { [EmbeddingModelProviders.OPENAI]: OpenAIEmbeddings, [EmbeddingModelProviders.COHEREAI]: CohereEmbeddings, [EmbeddingModelProviders.GOOGLE]: GoogleGenerativeAIEmbeddings, - [EmbeddingModelProviders.AZURE_OPENAI]: AzureOpenAIEmbeddings, + [EmbeddingModelProviders.AZURE_OPENAI]: OpenAIEmbeddings, [EmbeddingModelProviders.OLLAMA]: OllamaEmbeddings, [EmbeddingModelProviders.LM_STUDIO]: CustomOpenAIEmbeddings, [EmbeddingModelProviders.OPENAI_FORMAT]: OpenAIEmbeddings, @@ -248,16 +249,60 @@ export default class EmbeddingManager { modelName: modelName, apiKey: await getDecryptedKey(settings.googleApiKey), }, - [EmbeddingModelProviders.AZURE_OPENAI]: { - modelName, - azureOpenAIApiKey: await getDecryptedKey(customModel.apiKey || settings.azureOpenAIApiKey), - azureOpenAIApiInstanceName: - customModel.azureOpenAIApiInstanceName || settings.azureOpenAIApiInstanceName, - azureOpenAIApiDeploymentName: + [EmbeddingModelProviders.AZURE_OPENAI]: await (async () => { + const decryptedApiKey = await getDecryptedKey( + customModel.apiKey || settings.azureOpenAIApiKey + ); + + const hasCustomBaseUrl = Boolean(customModel.baseUrl?.trim()); + const apiVersion = customModel.azureOpenAIApiVersion || settings.azureOpenAIApiVersion; + + if (hasCustomBaseUrl) { + const azureUrl = normalizeAzureUrl(customModel.baseUrl!.trim()); + + return { + modelName, + apiKey: decryptedApiKey, + timeout: 10000, + batchSize: getSettings().embeddingBatchSize, + configuration: { + baseURL: azureUrl.baseUrl || customModel.baseUrl!.trim(), + defaultQuery: { + "api-version": azureUrl.apiVersion || apiVersion, + }, + fetch: customModel.enableCors ? safeFetch : undefined, + }, + } as ExtendedConfig< + ConstructorParameters< + EmbeddingProviderConstructorMap[typeof EmbeddingModelProviders.AZURE_OPENAI] + >[0] + >; + } + + const instanceName = + customModel.azureOpenAIApiInstanceName || settings.azureOpenAIApiInstanceName; + const deploymentName = customModel.azureOpenAIApiEmbeddingDeploymentName || - settings.azureOpenAIApiEmbeddingDeploymentName, - azureOpenAIApiVersion: customModel.azureOpenAIApiVersion || settings.azureOpenAIApiVersion, - }, + settings.azureOpenAIApiEmbeddingDeploymentName; + + return { + modelName, + apiKey: decryptedApiKey, + timeout: 10000, + batchSize: getSettings().embeddingBatchSize, + configuration: { + baseURL: `https://${instanceName}.openai.azure.com/openai/deployments/${deploymentName}`, + defaultQuery: { + "api-version": apiVersion, + }, + fetch: customModel.enableCors ? safeFetch : undefined, + }, + } as ExtendedConfig< + ConstructorParameters< + EmbeddingProviderConstructorMap[typeof EmbeddingModelProviders.AZURE_OPENAI] + >[0] + >; + })(), [EmbeddingModelProviders.OLLAMA]: { baseUrl: customModel.baseUrl || "http://localhost:11434", model: modelName, diff --git a/src/settings/v2/components/ModelAddDialog.tsx b/src/settings/v2/components/ModelAddDialog.tsx index 40d49065b..254043ed5 100644 --- a/src/settings/v2/components/ModelAddDialog.tsx +++ b/src/settings/v2/components/ModelAddDialog.tsx @@ -115,12 +115,12 @@ export const ModelAddDialog: React.FC = ({ if (!model.name) isValid = false; // Validate Azure OpenAI specific fields. - // Embedding models always require the legacy fields because EmbeddingManager - // does not consume baseUrl and still reads azureOpenAIApiInstanceName, - // azureOpenAIApiEmbeddingDeploymentName, and azureOpenAIApiVersion directly. - // Chat models may skip legacy fields when a full base URL is supplied instead. + // When a Base URL is provided, both chat and embedding models can + // skip the legacy Azure fields and rely on the normalized endpoint. + // When Base URL is empty, fall back to legacy instance/deployment/version + // fields for backward compatibility. const isAzure = model.provider === ChatModelProviders.AZURE_OPENAI; - const azureRequiresLegacyFields = isAzure && (isEmbeddingModel || !model.baseUrl?.trim()); + const azureRequiresLegacyFields = isAzure && !model.baseUrl?.trim(); if (azureRequiresLegacyFields) { newErrors.instanceName = !model.azureOpenAIApiInstanceName; newErrors.apiVersion = !model.azureOpenAIApiVersion; @@ -340,10 +340,10 @@ export const ModelAddDialog: React.FC = ({ ); case ChatModelProviders.AZURE_OPENAI: - // Chat models with a base URL use the new flow and skip legacy fields. - // Embedding models always require legacy fields since EmbeddingManager - // reads them directly and does not consume baseUrl. - if (model.baseUrl?.trim() && !isEmbeddingModel) return null; + // Chat and embedding models with a Base URL use the new flow + // and skip legacy fields. When Base URL is empty, fall back to + // legacy instance/deployment/version inputs. + if (model.baseUrl?.trim()) return null; return ( <> = ({ return providerInfo.host; } - const instanceName = model.azureOpenAIApiInstanceName || "[instance]"; - const deploymentName = isEmbeddingModel - ? model.azureOpenAIApiEmbeddingDeploymentName || "[deployment]" - : model.azureOpenAIApiDeploymentName || "[deployment]"; - const apiVersion = model.azureOpenAIApiVersion || "[api-version]"; - const endpoint = isEmbeddingModel ? "embeddings" : "chat/completions"; - - return `https://${instanceName}.openai.azure.com/openai/deployments/${deploymentName}/${endpoint}?api-version=${apiVersion}`; + // For Azure, encourage users to paste either the full endpoint URL + // (including /embeddings or /chat/completions and api-version) or + // the deployment base URL. Legacy instance/deployment/version fields + // are still available below when Base URL is empty. + return "https://[proxy]/deployments/[model]/embeddings?api-version=[version]"; }; const capabilityOptions = Object.entries(MODEL_CAPABILITIES).map(([id, description]) => ({