mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* Add GPT5 models * Refactor BUILTIN_CHAT_MODELS: remove some models * Fix temperature again
594 lines
21 KiB
TypeScript
594 lines
21 KiB
TypeScript
import { CustomModel, getModelKey, ModelConfig } from "@/aiParams";
|
|
import {
|
|
BREVILABS_API_BASE_URL,
|
|
BUILTIN_CHAT_MODELS,
|
|
ChatModelProviders,
|
|
ProviderInfo,
|
|
} from "@/constants";
|
|
import { getDecryptedKey } from "@/encryptionService";
|
|
import { logError, logInfo } from "@/logger";
|
|
import { getModelKeyFromModel, getSettings, subscribeToSettingsChange } from "@/settings/model";
|
|
import {
|
|
err2String,
|
|
getModelInfo,
|
|
ModelInfo,
|
|
safeFetch,
|
|
withSuppressedTokenWarnings,
|
|
} from "@/utils";
|
|
import { HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
|
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
import { ChatCohere } from "@langchain/cohere";
|
|
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
import { ChatDeepSeek } from "@langchain/deepseek";
|
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
import { ChatGroq } from "@langchain/groq";
|
|
import { ChatMistralAI } from "@langchain/mistralai";
|
|
import { ChatOllama } from "@langchain/ollama";
|
|
import { ChatOpenAI } from "@langchain/openai";
|
|
import { ChatXAI } from "@langchain/xai";
|
|
import { Notice } from "obsidian";
|
|
|
|
type ChatConstructorType = {
|
|
new (config: any): any;
|
|
};
|
|
|
|
const CHAT_PROVIDER_CONSTRUCTORS = {
|
|
[ChatModelProviders.OPENAI]: ChatOpenAI,
|
|
[ChatModelProviders.AZURE_OPENAI]: ChatOpenAI,
|
|
[ChatModelProviders.ANTHROPIC]: ChatAnthropic,
|
|
[ChatModelProviders.COHEREAI]: ChatCohere,
|
|
[ChatModelProviders.GOOGLE]: ChatGoogleGenerativeAI,
|
|
[ChatModelProviders.XAI]: ChatXAI,
|
|
[ChatModelProviders.OPENROUTERAI]: ChatOpenAI,
|
|
[ChatModelProviders.OLLAMA]: ChatOllama,
|
|
[ChatModelProviders.LM_STUDIO]: ChatOpenAI,
|
|
[ChatModelProviders.GROQ]: ChatGroq,
|
|
[ChatModelProviders.OPENAI_FORMAT]: ChatOpenAI,
|
|
[ChatModelProviders.COPILOT_PLUS]: ChatOpenAI,
|
|
[ChatModelProviders.MISTRAL]: ChatMistralAI,
|
|
[ChatModelProviders.DEEPSEEK]: ChatDeepSeek,
|
|
} as const;
|
|
|
|
type ChatProviderConstructMap = typeof CHAT_PROVIDER_CONSTRUCTORS;
|
|
|
|
export default class ChatModelManager {
|
|
private static instance: ChatModelManager;
|
|
private static chatModel: BaseChatModel | null;
|
|
private static modelMap: Record<
|
|
string,
|
|
{
|
|
hasApiKey: boolean;
|
|
AIConstructor: ChatConstructorType;
|
|
vendor: string;
|
|
}
|
|
>;
|
|
|
|
private readonly providerApiKeyMap: Record<ChatModelProviders, () => string> = {
|
|
[ChatModelProviders.OPENAI]: () => getSettings().openAIApiKey,
|
|
[ChatModelProviders.GOOGLE]: () => getSettings().googleApiKey,
|
|
[ChatModelProviders.AZURE_OPENAI]: () => getSettings().azureOpenAIApiKey,
|
|
[ChatModelProviders.ANTHROPIC]: () => getSettings().anthropicApiKey,
|
|
[ChatModelProviders.COHEREAI]: () => getSettings().cohereApiKey,
|
|
[ChatModelProviders.OPENROUTERAI]: () => getSettings().openRouterAiApiKey,
|
|
[ChatModelProviders.GROQ]: () => getSettings().groqApiKey,
|
|
[ChatModelProviders.XAI]: () => getSettings().xaiApiKey,
|
|
[ChatModelProviders.OLLAMA]: () => "default-key",
|
|
[ChatModelProviders.LM_STUDIO]: () => "default-key",
|
|
[ChatModelProviders.OPENAI_FORMAT]: () => "default-key",
|
|
[ChatModelProviders.COPILOT_PLUS]: () => getSettings().plusLicenseKey,
|
|
[ChatModelProviders.MISTRAL]: () => getSettings().mistralApiKey,
|
|
[ChatModelProviders.DEEPSEEK]: () => getSettings().deepseekApiKey,
|
|
} as const;
|
|
|
|
private constructor() {
|
|
this.buildModelMap();
|
|
subscribeToSettingsChange(() => {
|
|
this.buildModelMap();
|
|
this.validateCurrentModel();
|
|
});
|
|
}
|
|
|
|
static getInstance(): ChatModelManager {
|
|
if (!ChatModelManager.instance) {
|
|
ChatModelManager.instance = new ChatModelManager();
|
|
}
|
|
return ChatModelManager.instance;
|
|
}
|
|
|
|
private static readonly REASONING_MODEL_TEMPERATURE = 1;
|
|
|
|
/**
|
|
* Determines the appropriate temperature for a model
|
|
* @returns temperature value or undefined if temperature should not be set
|
|
*/
|
|
private getTemperatureForModel(
|
|
modelInfo: ModelInfo,
|
|
customModel: CustomModel,
|
|
settings: any
|
|
): number | undefined {
|
|
// Thinking-enabled models don't accept temperature
|
|
if (modelInfo.isThinkingEnabled) {
|
|
return undefined;
|
|
}
|
|
|
|
// O-series and GPT-5 models require temperature = 1
|
|
if (modelInfo.isOSeries || modelInfo.isGPT5) {
|
|
return ChatModelManager.REASONING_MODEL_TEMPERATURE;
|
|
}
|
|
|
|
// All other models use configured temperature
|
|
return customModel.temperature ?? settings.temperature;
|
|
}
|
|
|
|
private async getModelConfig(customModel: CustomModel): Promise<ModelConfig> {
|
|
const settings = getSettings();
|
|
|
|
const modelName = customModel.name;
|
|
const modelInfo = getModelInfo(modelName);
|
|
const { isThinkingEnabled } = modelInfo;
|
|
|
|
// Base config - temperature will be handled by provider-specific methods
|
|
const baseConfig: Omit<ModelConfig, "maxTokens" | "maxCompletionTokens"> = {
|
|
modelName: modelName,
|
|
streaming: customModel.stream ?? true,
|
|
maxRetries: 3,
|
|
maxConcurrency: 3,
|
|
enableCors: customModel.enableCors,
|
|
// Add temperature for normal models (will be overridden by special configs if needed)
|
|
...(!isThinkingEnabled && { temperature: customModel.temperature ?? settings.temperature }),
|
|
};
|
|
|
|
const providerConfig: {
|
|
[K in keyof ChatProviderConstructMap]: ConstructorParameters<ChatProviderConstructMap[K]>[0];
|
|
} = {
|
|
[ChatModelProviders.OPENAI]: {
|
|
modelName: modelName,
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.openAIApiKey),
|
|
configuration: {
|
|
baseURL: customModel.baseUrl,
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
organization: await getDecryptedKey(customModel.openAIOrgId || settings.openAIOrgId),
|
|
},
|
|
...this.getOpenAISpecialConfig(
|
|
modelName,
|
|
customModel.maxTokens ?? settings.maxTokens,
|
|
customModel.temperature ?? settings.temperature,
|
|
customModel
|
|
),
|
|
},
|
|
[ChatModelProviders.ANTHROPIC]: {
|
|
anthropicApiKey: await getDecryptedKey(customModel.apiKey || settings.anthropicApiKey),
|
|
model: modelName,
|
|
anthropicApiUrl: customModel.baseUrl,
|
|
clientOptions: {
|
|
// Required to bypass CORS restrictions
|
|
defaultHeaders: {
|
|
"anthropic-dangerous-direct-browser-access": "true",
|
|
},
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
},
|
|
...(isThinkingEnabled && {
|
|
thinking: { type: "enabled", budget_tokens: 2048 },
|
|
}),
|
|
},
|
|
[ChatModelProviders.AZURE_OPENAI]: {
|
|
modelName:
|
|
customModel.azureOpenAIApiDeploymentName || settings.azureOpenAIApiDeploymentName,
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.azureOpenAIApiKey),
|
|
configuration: {
|
|
baseURL:
|
|
customModel.baseUrl ||
|
|
`https://${customModel.azureOpenAIApiInstanceName || settings.azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${customModel.azureOpenAIApiDeploymentName || settings.azureOpenAIApiDeploymentName}`,
|
|
defaultQuery: {
|
|
"api-version": customModel.azureOpenAIApiVersion || settings.azureOpenAIApiVersion,
|
|
},
|
|
defaultHeaders: {
|
|
"Content-Type": "application/json",
|
|
"api-key": await getDecryptedKey(customModel.apiKey || settings.azureOpenAIApiKey),
|
|
},
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
},
|
|
...this.getOpenAISpecialConfig(
|
|
modelName,
|
|
customModel.maxTokens ?? settings.maxTokens,
|
|
customModel.temperature ?? settings.temperature,
|
|
customModel
|
|
),
|
|
},
|
|
[ChatModelProviders.COHEREAI]: {
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.cohereApiKey),
|
|
model: modelName,
|
|
},
|
|
[ChatModelProviders.GOOGLE]: {
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.googleApiKey),
|
|
modelName: modelName,
|
|
safetySettings: [
|
|
{
|
|
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
},
|
|
{
|
|
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
},
|
|
{
|
|
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
},
|
|
{
|
|
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
threshold: HarmBlockThreshold.BLOCK_NONE,
|
|
},
|
|
],
|
|
baseUrl: customModel.baseUrl,
|
|
},
|
|
[ChatModelProviders.XAI]: {
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.xaiApiKey),
|
|
model: modelName,
|
|
// This langchainjs XAI client does not support baseURL override
|
|
},
|
|
[ChatModelProviders.OPENROUTERAI]: {
|
|
modelName: modelName,
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.openRouterAiApiKey),
|
|
configuration: {
|
|
baseURL: customModel.baseUrl || "https://openrouter.ai/api/v1",
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
defaultHeaders: {
|
|
"HTTP-Referer": "https://obsidiancopilot.com",
|
|
"X-Title": "Obsidian Copilot",
|
|
},
|
|
},
|
|
},
|
|
[ChatModelProviders.GROQ]: {
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.groqApiKey),
|
|
modelName: modelName,
|
|
},
|
|
[ChatModelProviders.OLLAMA]: {
|
|
// ChatOllama has `model` instead of `modelName`!!
|
|
model: modelName,
|
|
// MUST NOT use /v1 in the baseUrl for ollama
|
|
baseUrl: customModel.baseUrl || "http://localhost:11434",
|
|
headers: new Headers({
|
|
Authorization: `Bearer ${await getDecryptedKey(customModel.apiKey || "default-key")}`,
|
|
}),
|
|
},
|
|
[ChatModelProviders.LM_STUDIO]: {
|
|
modelName: modelName,
|
|
apiKey: customModel.apiKey || "default-key",
|
|
configuration: {
|
|
baseURL: customModel.baseUrl || "http://localhost:1234/v1",
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
},
|
|
},
|
|
[ChatModelProviders.OPENAI_FORMAT]: {
|
|
modelName: modelName,
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.openAIApiKey),
|
|
configuration: {
|
|
baseURL: customModel.baseUrl,
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
defaultHeaders: { "dangerously-allow-browser": "true" },
|
|
},
|
|
...this.getOpenAISpecialConfig(
|
|
modelName,
|
|
customModel.maxTokens ?? settings.maxTokens,
|
|
customModel.temperature ?? settings.temperature,
|
|
customModel
|
|
),
|
|
},
|
|
[ChatModelProviders.COPILOT_PLUS]: {
|
|
modelName: modelName,
|
|
apiKey: await getDecryptedKey(settings.plusLicenseKey),
|
|
configuration: {
|
|
baseURL: BREVILABS_API_BASE_URL,
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
},
|
|
},
|
|
[ChatModelProviders.MISTRAL]: {
|
|
model: modelName,
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.mistralApiKey),
|
|
serverURL: customModel.baseUrl,
|
|
},
|
|
[ChatModelProviders.DEEPSEEK]: {
|
|
modelName: modelName,
|
|
apiKey: await getDecryptedKey(customModel.apiKey || settings.deepseekApiKey),
|
|
configuration: {
|
|
baseURL: customModel.baseUrl || ProviderInfo[ChatModelProviders.DEEPSEEK].host,
|
|
fetch: customModel.enableCors ? safeFetch : undefined,
|
|
},
|
|
},
|
|
};
|
|
|
|
const selectedProviderConfig =
|
|
providerConfig[customModel.provider as keyof typeof providerConfig] || {};
|
|
|
|
// Get provider-specific parameters (like topP, frequencyPenalty) that the provider supports
|
|
const providerSpecificParams = this.getProviderSpecificParams(
|
|
customModel.provider as ChatModelProviders,
|
|
customModel
|
|
);
|
|
|
|
// LangChain 0.6.6 handles token configuration for special models internally
|
|
const tokenConfig = isThinkingEnabled
|
|
? {
|
|
maxTokens: customModel.maxTokens ?? settings.maxTokens,
|
|
}
|
|
: {
|
|
maxTokens: customModel.maxTokens ?? settings.maxTokens,
|
|
};
|
|
|
|
const finalConfig = {
|
|
...baseConfig,
|
|
...selectedProviderConfig,
|
|
...providerSpecificParams,
|
|
...tokenConfig,
|
|
};
|
|
|
|
return finalConfig as ModelConfig;
|
|
}
|
|
|
|
/**
|
|
* Adds special configuration for OpenAI models that support reasoning
|
|
* LangChain 0.6.6+ handles most of the token/temperature logic internally
|
|
*/
|
|
private getOpenAISpecialConfig(
|
|
modelName: string,
|
|
maxTokens: number,
|
|
_temperature: number | undefined,
|
|
customModel?: CustomModel
|
|
) {
|
|
const settings = getSettings();
|
|
const modelInfo = getModelInfo(modelName);
|
|
const resolvedTemperature = this.getTemperatureForModel(
|
|
modelInfo,
|
|
customModel || ({} as CustomModel),
|
|
settings
|
|
);
|
|
|
|
const config: any = {
|
|
maxTokens,
|
|
temperature: resolvedTemperature,
|
|
};
|
|
|
|
// Add reasoning parameters for O-series and GPT-5 models
|
|
// LangChain 0.6.6 will handle the endpoint routing and parameter conversion
|
|
if (modelInfo.isOSeries || modelInfo.isGPT5) {
|
|
config.reasoning = {
|
|
effort: customModel?.reasoningEffort || settings.reasoningEffort || "low",
|
|
};
|
|
|
|
// Add verbosity for GPT-5 models (when API supports it)
|
|
if (modelInfo.isGPT5) {
|
|
const verbosityValue = customModel?.verbosity || settings.verbosity || "medium";
|
|
config.verbosity = verbosityValue;
|
|
config.modelKwargs = { verbosity: verbosityValue };
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Returns provider-specific parameters (like topP, frequencyPenalty) based on what the provider supports
|
|
* This prevents passing undefined values to providers that don't support them
|
|
*/
|
|
private getProviderSpecificParams(provider: ChatModelProviders, customModel: CustomModel) {
|
|
const params: Record<string, any> = {};
|
|
|
|
// Add topP only if defined
|
|
if (customModel.topP !== undefined) {
|
|
// These providers support topP
|
|
if (
|
|
[
|
|
ChatModelProviders.OPENAI,
|
|
ChatModelProviders.AZURE_OPENAI,
|
|
ChatModelProviders.ANTHROPIC,
|
|
ChatModelProviders.GOOGLE,
|
|
ChatModelProviders.OPENROUTERAI,
|
|
ChatModelProviders.OLLAMA,
|
|
ChatModelProviders.LM_STUDIO,
|
|
ChatModelProviders.OPENAI_FORMAT,
|
|
ChatModelProviders.MISTRAL,
|
|
ChatModelProviders.DEEPSEEK,
|
|
].includes(provider)
|
|
) {
|
|
params.topP = customModel.topP;
|
|
}
|
|
}
|
|
|
|
// Add frequencyPenalty only if defined
|
|
if (customModel.frequencyPenalty !== undefined) {
|
|
// These providers support frequencyPenalty
|
|
if (
|
|
[
|
|
ChatModelProviders.OPENAI,
|
|
ChatModelProviders.AZURE_OPENAI,
|
|
ChatModelProviders.OPENROUTERAI,
|
|
ChatModelProviders.OLLAMA,
|
|
ChatModelProviders.LM_STUDIO,
|
|
ChatModelProviders.OPENAI_FORMAT,
|
|
ChatModelProviders.MISTRAL,
|
|
ChatModelProviders.DEEPSEEK,
|
|
].includes(provider)
|
|
) {
|
|
params.frequencyPenalty = customModel.frequencyPenalty;
|
|
}
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
// Build a map of modelKey to model config
|
|
public buildModelMap() {
|
|
const activeModels = getSettings().activeModels;
|
|
ChatModelManager.modelMap = {};
|
|
const modelMap = ChatModelManager.modelMap;
|
|
|
|
const allModels = activeModels ?? BUILTIN_CHAT_MODELS;
|
|
|
|
allModels.forEach((model) => {
|
|
if (model.enabled) {
|
|
if (!Object.values(ChatModelProviders).contains(model.provider as ChatModelProviders)) {
|
|
console.warn(`Unknown provider: ${model.provider} for model: ${model.name}`);
|
|
return;
|
|
}
|
|
|
|
const constructor = this.getProviderConstructor(model);
|
|
const getDefaultApiKey = this.providerApiKeyMap[model.provider as ChatModelProviders];
|
|
|
|
const apiKey = model.apiKey || getDefaultApiKey();
|
|
const modelKey = getModelKeyFromModel(model);
|
|
modelMap[modelKey] = {
|
|
hasApiKey: Boolean(model.apiKey || apiKey),
|
|
AIConstructor: constructor,
|
|
vendor: model.provider,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
getProviderConstructor(model: CustomModel): ChatConstructorType {
|
|
const constructor: ChatConstructorType =
|
|
CHAT_PROVIDER_CONSTRUCTORS[model.provider as ChatModelProviders];
|
|
if (!constructor) {
|
|
console.warn(`Unknown provider: ${model.provider} for model: ${model.name}`);
|
|
throw new Error(`Unknown provider: ${model.provider} for model: ${model.name}`);
|
|
}
|
|
return constructor;
|
|
}
|
|
|
|
getChatModel(): BaseChatModel {
|
|
if (!ChatModelManager.chatModel) {
|
|
throw new Error("No valid chat model available. Please check your API key settings.");
|
|
}
|
|
return ChatModelManager.chatModel;
|
|
}
|
|
|
|
async setChatModel(model: CustomModel): Promise<void> {
|
|
const modelKey = getModelKeyFromModel(model);
|
|
try {
|
|
const modelInstance = await this.createModelInstance(model);
|
|
ChatModelManager.chatModel = modelInstance;
|
|
} catch (error) {
|
|
logError(error);
|
|
new Notice(`Error creating model: ${modelKey}`);
|
|
}
|
|
}
|
|
|
|
async createModelInstance(model: CustomModel): Promise<BaseChatModel> {
|
|
// Create and return the appropriate model
|
|
const modelKey = getModelKeyFromModel(model);
|
|
const selectedModel = ChatModelManager.modelMap[modelKey];
|
|
if (!selectedModel) {
|
|
throw new Error(`No model found for: ${modelKey}`);
|
|
}
|
|
if (!selectedModel.hasApiKey) {
|
|
const errorMessage = `API key is not provided for the model: ${modelKey}.`;
|
|
new Notice(errorMessage);
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
const modelConfig = await this.getModelConfig(model);
|
|
|
|
const newModelInstance = new selectedModel.AIConstructor({
|
|
...modelConfig,
|
|
});
|
|
return newModelInstance;
|
|
}
|
|
|
|
validateChatModel(chatModel: BaseChatModel): boolean {
|
|
if (chatModel === undefined || chatModel === null) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Custom token estimation function for fallback when model is unknown
|
|
private estimateTokens(text: string): number {
|
|
if (!text) return 0;
|
|
// This is a simple approximation: ~4 chars per token for English text
|
|
// More accurate than using word count, but still a decent estimation
|
|
return Math.ceil(text.length / 4);
|
|
}
|
|
|
|
async countTokens(inputStr: string): Promise<number> {
|
|
try {
|
|
return await withSuppressedTokenWarnings(async () => {
|
|
return ChatModelManager.chatModel?.getNumTokens(inputStr) ?? 0;
|
|
});
|
|
} catch (error) {
|
|
// If there's an error calculating tokens, use a simple approximation instead
|
|
// This prevents "Unknown model" errors from appearing in the console
|
|
if (error instanceof Error && error.message.includes("Unknown model")) {
|
|
// Simple approximation: 1 token ~= 4 characters for English text
|
|
logInfo("Using estimated token count due to tokenizer error");
|
|
// Fall back to our estimation if LangChain's method fails
|
|
return this.estimateTokens(inputStr);
|
|
}
|
|
// For other errors, rethrow
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private validateCurrentModel(): void {
|
|
if (!ChatModelManager.chatModel) return;
|
|
|
|
const currentModelKey = getModelKey();
|
|
if (!currentModelKey) return;
|
|
|
|
// Get the model configuration
|
|
const selectedModel = ChatModelManager.modelMap[currentModelKey];
|
|
|
|
// If API key is missing or model doesn't exist in map
|
|
if (!selectedModel?.hasApiKey) {
|
|
// Clear the current chat model
|
|
ChatModelManager.chatModel = null;
|
|
console.log("Failed to reinitialize model due to missing API key");
|
|
}
|
|
}
|
|
|
|
async ping(model: CustomModel): Promise<boolean> {
|
|
const tryPing = async (enableCors: boolean) => {
|
|
const modelToTest = { ...model, enableCors };
|
|
const modelConfig = await this.getModelConfig(modelToTest);
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { streaming, maxTokens, maxCompletionTokens, ...pingConfig } = modelConfig;
|
|
// For ping, just use minimal config
|
|
const tokenConfig = { maxTokens: 30 };
|
|
const testModel = new (this.getProviderConstructor(modelToTest))({
|
|
...pingConfig,
|
|
...tokenConfig,
|
|
});
|
|
await testModel.invoke([{ role: "user", content: "hello" }], {
|
|
timeout: 8000,
|
|
});
|
|
};
|
|
|
|
try {
|
|
// First try without CORS
|
|
await tryPing(false);
|
|
return true;
|
|
} catch (firstError) {
|
|
console.log("First ping attempt failed, trying with CORS...");
|
|
try {
|
|
// Second try with CORS
|
|
await tryPing(true);
|
|
new Notice(
|
|
"Connection successful, but requires CORS to be enabled. Please enable CORS for this model once you add it above."
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
const msg =
|
|
"\nwithout CORS Error: " +
|
|
err2String(firstError) +
|
|
"\nwith CORS Error: " +
|
|
err2String(error);
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
findModelByName(modelName: string): CustomModel | undefined {
|
|
const settings = getSettings();
|
|
return settings.activeModels.find((model) => model.name === modelName);
|
|
}
|
|
}
|