mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 19:12:27 +08:00
add Voyage AI as Embedding provider
This commit is contained in:
parent
558f6a9802
commit
c37201183b
@ -201,9 +201,10 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
|
|
||||||
<nav class="options-section-tabs">
|
<nav class="options-section-tabs">
|
||||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||||
<button class="nav-link active" id="nav-openai-tab" data-bs-toggle="tab" data-bs-target="#nav-openai" type="button" role="tab" aria-controls="nav-openai" aria-selected="true">OpenAI</button>
|
<button class="nav-link active" id="nav-openai-tab" data-bs-toggle="tab" data-bs-target="#nav-openai" type="button" role="tab" aria-controls="nav-openai" aria-selected="true">${t("ai_llm.openai_tab")}</button>
|
||||||
<button class="nav-link" id="nav-anthropic-tab" data-bs-toggle="tab" data-bs-target="#nav-anthropic" type="button" role="tab" aria-controls="nav-anthropic" aria-selected="false">Anthropic</button>
|
<button class="nav-link" id="nav-anthropic-tab" data-bs-toggle="tab" data-bs-target="#nav-anthropic" type="button" role="tab" aria-controls="nav-anthropic" aria-selected="false">${t("ai_llm.anthropic_tab")}</button>
|
||||||
<button class="nav-link" id="nav-ollama-tab" data-bs-toggle="tab" data-bs-target="#nav-ollama" type="button" role="tab" aria-controls="nav-ollama" aria-selected="false">Ollama</button>
|
<button class="nav-link" id="nav-voyage-tab" data-bs-toggle="tab" data-bs-target="#nav-voyage" type="button" role="tab" aria-controls="nav-voyage" aria-selected="false">${t("ai_llm.voyage_tab")}</button>
|
||||||
|
<button class="nav-link" id="nav-ollama-tab" data-bs-toggle="tab" data-bs-target="#nav-ollama" type="button" role="tab" aria-controls="nav-ollama" aria-selected="false">${t("ai_llm.ollama_tab")}</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="options-section">
|
<div class="options-section">
|
||||||
@ -280,6 +281,29 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-pane fade" id="nav-voyage" role="tabpanel" aria-labelledby="nav-voyage-tab">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>${t("ai_llm.voyage_configuration")}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${t("ai_llm.api_key")}</label>
|
||||||
|
<input type="password" class="voyage-api-key form-control" autocomplete="off">
|
||||||
|
<div class="form-text">${t("ai_llm.voyage_api_key_description")}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${t("ai_llm.embedding_model")}</label>
|
||||||
|
<select class="voyage-embedding-model form-control">
|
||||||
|
<option value="voyage-2">voyage-2 (recommended)</option>
|
||||||
|
<option value="voyage-large-2">voyage-large-2</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">${t("ai_llm.voyage_embedding_model_description")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tab-pane fade" id="nav-ollama" role="tabpanel" aria-labelledby="nav-ollama-tab">
|
<div class="tab-pane fade" id="nav-ollama" role="tabpanel" aria-labelledby="nav-ollama-tab">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -333,7 +357,7 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
<label>${t("ai_llm.embedding_default_provider")}</label>
|
<label>${t("ai_llm.embedding_default_provider")}</label>
|
||||||
<select class="embedding-default-provider form-control">
|
<select class="embedding-default-provider form-control">
|
||||||
<option value="openai">OpenAI</option>
|
<option value="openai">OpenAI</option>
|
||||||
<option value="anthropic">Anthropic</option>
|
<option value="voyage">Voyage AI</option>
|
||||||
<option value="ollama">Ollama</option>
|
<option value="ollama">Ollama</option>
|
||||||
<option value="local">Local</option>
|
<option value="local">Local</option>
|
||||||
</select>
|
</select>
|
||||||
@ -366,16 +390,16 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
<span class="bx bx-x"></span>
|
<span class="bx bx-x"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="standard-list-item d-flex align-items-center" data-provider="ollama">
|
<li class="standard-list-item d-flex align-items-center" data-provider="voyage">
|
||||||
<span class="bx bx-menu handle me-2"></span>
|
<span class="bx bx-menu handle me-2"></span>
|
||||||
<strong class="flex-grow-1">Ollama</strong>
|
<strong class="flex-grow-1">Voyage AI</strong>
|
||||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||||
<span class="bx bx-x"></span>
|
<span class="bx bx-x"></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="standard-list-item d-flex align-items-center" data-provider="anthropic">
|
<li class="standard-list-item d-flex align-items-center" data-provider="ollama">
|
||||||
<span class="bx bx-menu handle me-2"></span>
|
<span class="bx bx-menu handle me-2"></span>
|
||||||
<strong class="flex-grow-1">Anthropic</strong>
|
<strong class="flex-grow-1">Ollama</strong>
|
||||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||||
<span class="bx bx-x"></span>
|
<span class="bx bx-x"></span>
|
||||||
</button>
|
</button>
|
||||||
@ -560,6 +584,16 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
await this.updateOption('anthropicBaseUrl', $anthropicBaseUrl.val() as string);
|
await this.updateOption('anthropicBaseUrl', $anthropicBaseUrl.val() as string);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $voyageApiKey = this.$widget.find('.voyage-api-key');
|
||||||
|
$voyageApiKey.on('change', async () => {
|
||||||
|
await this.updateOption('voyageApiKey', $voyageApiKey.val() as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
const $voyageEmbeddingModel = this.$widget.find('.voyage-embedding-model');
|
||||||
|
$voyageEmbeddingModel.on('change', async () => {
|
||||||
|
await this.updateOption('voyageEmbeddingModel', $voyageEmbeddingModel.val() as string);
|
||||||
|
});
|
||||||
|
|
||||||
const $ollamaBaseUrl = this.$widget.find('.ollama-base-url');
|
const $ollamaBaseUrl = this.$widget.find('.ollama-base-url');
|
||||||
$ollamaBaseUrl.on('change', async () => {
|
$ollamaBaseUrl.on('change', async () => {
|
||||||
await this.updateOption('ollamaBaseUrl', $ollamaBaseUrl.val() as string);
|
await this.updateOption('ollamaBaseUrl', $ollamaBaseUrl.val() as string);
|
||||||
@ -1001,7 +1035,7 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
if (!savedValue) return;
|
if (!savedValue) return;
|
||||||
|
|
||||||
// Get all available providers
|
// Get all available providers
|
||||||
const allProviders = ['openai', 'anthropic', 'ollama', 'local'];
|
const allProviders = ['openai', 'voyage', 'anthropic', 'ollama', 'local'];
|
||||||
const savedProviders = savedValue.split(',');
|
const savedProviders = savedValue.split(',');
|
||||||
|
|
||||||
// Find disabled providers (providers in allProviders but not in savedProviders)
|
// Find disabled providers (providers in allProviders but not in savedProviders)
|
||||||
@ -1153,6 +1187,10 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
this.$widget.find('.anthropic-default-model').val(options.anthropicDefaultModel || 'claude-3-opus-20240229');
|
this.$widget.find('.anthropic-default-model').val(options.anthropicDefaultModel || 'claude-3-opus-20240229');
|
||||||
this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com/v1');
|
this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com/v1');
|
||||||
|
|
||||||
|
// Voyage Section
|
||||||
|
this.$widget.find('.voyage-api-key').val(options.voyageApiKey || '');
|
||||||
|
this.$widget.find('.voyage-embedding-model').val(options.voyageEmbeddingModel || 'voyage-2');
|
||||||
|
|
||||||
// Ollama Section
|
// Ollama Section
|
||||||
this.$widget.find('.ollama-enabled').prop('checked', options.ollamaEnabled !== 'false');
|
this.$widget.find('.ollama-enabled').prop('checked', options.ollamaEnabled !== 'false');
|
||||||
this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434');
|
this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434');
|
||||||
@ -1847,7 +1885,7 @@ export default class AiSettingsWidget extends OptionsWidget {
|
|||||||
if (!savedValue) return;
|
if (!savedValue) return;
|
||||||
|
|
||||||
// Get all available providers
|
// Get all available providers
|
||||||
const allProviders = ['openai', 'anthropic', 'ollama'];
|
const allProviders = ['openai', 'voyage', 'anthropic', 'ollama'];
|
||||||
const savedProviders = savedValue.split(',');
|
const savedProviders = savedValue.split(',');
|
||||||
|
|
||||||
// Find disabled providers (providers in allProviders but not in savedProviders)
|
// Find disabled providers (providers in allProviders but not in savedProviders)
|
||||||
|
@ -1122,7 +1122,11 @@
|
|||||||
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
||||||
},
|
},
|
||||||
"ai_llm": {
|
"ai_llm": {
|
||||||
"title": "AI/LLM Integration",
|
"title": "AI & Embedding Settings",
|
||||||
|
"openai_tab": "OpenAI",
|
||||||
|
"anthropic_tab": "Anthropic",
|
||||||
|
"voyage_tab": "Voyage AI",
|
||||||
|
"ollama_tab": "Ollama",
|
||||||
"enable_ai": "Enable AI/LLM features",
|
"enable_ai": "Enable AI/LLM features",
|
||||||
"enable_ai_desc": "Enable AI features like note summarization, content generation, and other LLM capabilities",
|
"enable_ai_desc": "Enable AI features like note summarization, content generation, and other LLM capabilities",
|
||||||
"enable_ai_features": "Enable AI/LLM features",
|
"enable_ai_features": "Enable AI/LLM features",
|
||||||
@ -1149,8 +1153,10 @@
|
|||||||
"openai_url_description": "Default: https://api.openai.com/v1",
|
"openai_url_description": "Default: https://api.openai.com/v1",
|
||||||
"anthropic_configuration": "Anthropic Configuration",
|
"anthropic_configuration": "Anthropic Configuration",
|
||||||
"anthropic_model_description": "Examples: claude-3-opus-20240229, claude-3-sonnet-20240229",
|
"anthropic_model_description": "Examples: claude-3-opus-20240229, claude-3-sonnet-20240229",
|
||||||
"anthropic_embedding_model_description": "Anthropic embedding model (not available yet)",
|
"voyage_embedding_model_description": "Voyage AI embedding models for text embeddings (voyage-2 recommended)",
|
||||||
"anthropic_url_description": "Default: https://api.anthropic.com/v1",
|
"voyage_configuration": "Voyage AI Configuration",
|
||||||
|
"voyage_api_key_description": "Your Voyage AI API key for generating embeddings",
|
||||||
|
"voyage_url_description": "Default: https://api.voyageai.com/v1",
|
||||||
"ollama_configuration": "Ollama Configuration",
|
"ollama_configuration": "Ollama Configuration",
|
||||||
"enable_ollama": "Enable Ollama",
|
"enable_ollama": "Enable Ollama",
|
||||||
"enable_ollama_description": "Enable Ollama for local AI model usage",
|
"enable_ollama_description": "Enable Ollama for local AI model usage",
|
||||||
|
@ -21,6 +21,7 @@ export const LLM_CONSTANTS = {
|
|||||||
OLLAMA: 6000,
|
OLLAMA: 6000,
|
||||||
OPENAI: 12000,
|
OPENAI: 12000,
|
||||||
ANTHROPIC: 15000,
|
ANTHROPIC: 15000,
|
||||||
|
VOYAGE: 12000,
|
||||||
DEFAULT: 6000
|
DEFAULT: 6000
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -38,6 +39,9 @@ export const LLM_CONSTANTS = {
|
|||||||
ANTHROPIC: {
|
ANTHROPIC: {
|
||||||
CLAUDE: 1024,
|
CLAUDE: 1024,
|
||||||
DEFAULT: 1024
|
DEFAULT: 1024
|
||||||
|
},
|
||||||
|
VOYAGE: {
|
||||||
|
DEFAULT: 1024
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -87,7 +87,8 @@ const ALLOWED_OPTIONS = new Set([
|
|||||||
"openaiBaseUrl",
|
"openaiBaseUrl",
|
||||||
"anthropicApiKey",
|
"anthropicApiKey",
|
||||||
"anthropicDefaultModel",
|
"anthropicDefaultModel",
|
||||||
"anthropicEmbeddingModel",
|
"voyageEmbeddingModel",
|
||||||
|
"voyageApiKey",
|
||||||
"anthropicBaseUrl",
|
"anthropicBaseUrl",
|
||||||
"ollamaEnabled",
|
"ollamaEnabled",
|
||||||
"ollamaBaseUrl",
|
"ollamaBaseUrl",
|
||||||
|
@ -6,7 +6,8 @@ import { randomString } from "../../utils.js";
|
|||||||
import type { EmbeddingProvider, EmbeddingConfig } from "./embeddings_interface.js";
|
import type { EmbeddingProvider, EmbeddingConfig } from "./embeddings_interface.js";
|
||||||
import { OpenAIEmbeddingProvider } from "./providers/openai.js";
|
import { OpenAIEmbeddingProvider } from "./providers/openai.js";
|
||||||
import { OllamaEmbeddingProvider } from "./providers/ollama.js";
|
import { OllamaEmbeddingProvider } from "./providers/ollama.js";
|
||||||
import { AnthropicEmbeddingProvider } from "./providers/anthropic.js";
|
import { VoyageEmbeddingProvider } from "./providers/voyage.js";
|
||||||
|
import type { OptionDefinitions } from "../../options_interface.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple local embedding provider implementation
|
* Simple local embedding provider implementation
|
||||||
@ -250,29 +251,29 @@ export async function initializeDefaultProviders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Anthropic provider if API key is configured
|
// Register Voyage provider if API key is configured
|
||||||
const anthropicApiKey = await options.getOption('anthropicApiKey');
|
const voyageApiKey = await options.getOption('voyageApiKey' as any);
|
||||||
if (anthropicApiKey) {
|
if (voyageApiKey) {
|
||||||
const anthropicModel = await options.getOption('anthropicDefaultModel') || 'claude-3-haiku-20240307';
|
const voyageModel = await options.getOption('voyageEmbeddingModel') || 'voyage-2';
|
||||||
const anthropicBaseUrl = await options.getOption('anthropicBaseUrl') || 'https://api.anthropic.com/v1';
|
const voyageBaseUrl = 'https://api.voyageai.com/v1';
|
||||||
|
|
||||||
registerEmbeddingProvider(new AnthropicEmbeddingProvider({
|
registerEmbeddingProvider(new VoyageEmbeddingProvider({
|
||||||
model: anthropicModel,
|
model: voyageModel,
|
||||||
dimension: 1024, // Anthropic's embedding dimension
|
dimension: 1024, // Voyage's embedding dimension
|
||||||
type: 'float32',
|
type: 'float32',
|
||||||
apiKey: anthropicApiKey,
|
apiKey: voyageApiKey,
|
||||||
baseUrl: anthropicBaseUrl
|
baseUrl: voyageBaseUrl
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create Anthropic provider config if it doesn't exist
|
// Create Voyage provider config if it doesn't exist
|
||||||
const existingAnthropic = await sql.getRow(
|
const existingVoyage = await sql.getRow(
|
||||||
"SELECT * FROM embedding_providers WHERE name = ?",
|
"SELECT * FROM embedding_providers WHERE name = ?",
|
||||||
['anthropic']
|
['voyage']
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingAnthropic) {
|
if (!existingVoyage) {
|
||||||
await createEmbeddingProviderConfig('anthropic', {
|
await createEmbeddingProviderConfig('voyage', {
|
||||||
model: anthropicModel,
|
model: voyageModel,
|
||||||
dimension: 1024,
|
dimension: 1024,
|
||||||
type: 'float32'
|
type: 'float32'
|
||||||
}, true, 75);
|
}, true, 75);
|
||||||
|
@ -1,218 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import log from "../../../log.js";
|
|
||||||
import { BaseEmbeddingProvider } from "../base_embeddings.js";
|
|
||||||
import type { EmbeddingConfig, EmbeddingModelInfo } from "../embeddings_interface.js";
|
|
||||||
import { LLM_CONSTANTS } from "../../../../routes/api/llm.js";
|
|
||||||
|
|
||||||
// Anthropic model context window sizes - as of current API version
|
|
||||||
const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
||||||
"claude-3-opus-20240229": 200000,
|
|
||||||
"claude-3-sonnet-20240229": 180000,
|
|
||||||
"claude-3-haiku-20240307": 48000,
|
|
||||||
"claude-2.1": 200000,
|
|
||||||
"claude-2.0": 100000,
|
|
||||||
"claude-instant-1.2": 100000,
|
|
||||||
"default": 100000
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Anthropic embedding provider implementation
|
|
||||||
*/
|
|
||||||
export class AnthropicEmbeddingProvider extends BaseEmbeddingProvider {
|
|
||||||
name = "anthropic";
|
|
||||||
|
|
||||||
constructor(config: EmbeddingConfig) {
|
|
||||||
super(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the provider by detecting model capabilities
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
const modelName = this.config.model || "claude-3-haiku-20240307";
|
|
||||||
try {
|
|
||||||
// Detect model capabilities
|
|
||||||
const modelInfo = await this.getModelInfo(modelName);
|
|
||||||
|
|
||||||
// Update the config dimension
|
|
||||||
this.config.dimension = modelInfo.dimension;
|
|
||||||
|
|
||||||
log.info(`Anthropic model ${modelName} initialized with dimension ${this.config.dimension} and context window ${modelInfo.contextWindow}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error initializing Anthropic provider: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to determine Anthropic model capabilities
|
|
||||||
* Note: Anthropic doesn't have a public endpoint for model metadata, so we use a combination
|
|
||||||
* of known values and detection by test embeddings
|
|
||||||
*/
|
|
||||||
private async fetchModelCapabilities(modelName: string): Promise<EmbeddingModelInfo | null> {
|
|
||||||
// Anthropic doesn't have a model info endpoint, but we can look up known context sizes
|
|
||||||
// and detect embedding dimensions by making a test request
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get context window size from our local registry of known models
|
|
||||||
const modelBase = Object.keys(ANTHROPIC_MODEL_CONTEXT_WINDOWS).find(
|
|
||||||
model => modelName.startsWith(model)
|
|
||||||
) || "default";
|
|
||||||
|
|
||||||
const contextWindow = ANTHROPIC_MODEL_CONTEXT_WINDOWS[modelBase];
|
|
||||||
|
|
||||||
// For embedding dimension, we'll return null and let getModelInfo detect it
|
|
||||||
return {
|
|
||||||
dimension: 0, // Will be detected by test embedding
|
|
||||||
contextWindow
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
log.info(`Could not determine capabilities for Anthropic model ${modelName}: ${error}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get model information including embedding dimensions
|
|
||||||
*/
|
|
||||||
async getModelInfo(modelName: string): Promise<EmbeddingModelInfo> {
|
|
||||||
// Check cache first
|
|
||||||
if (this.modelInfoCache.has(modelName)) {
|
|
||||||
return this.modelInfoCache.get(modelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to determine model capabilities
|
|
||||||
const capabilities = await this.fetchModelCapabilities(modelName);
|
|
||||||
const contextWindow = capabilities?.contextWindow || LLM_CONSTANTS.CONTEXT_WINDOW.ANTHROPIC;
|
|
||||||
|
|
||||||
// For Anthropic, we need to detect embedding dimension with a test call
|
|
||||||
try {
|
|
||||||
// Detect dimension with a test embedding
|
|
||||||
const testEmbedding = await this.generateEmbeddings("Test");
|
|
||||||
const dimension = testEmbedding.length;
|
|
||||||
|
|
||||||
const modelInfo: EmbeddingModelInfo = {
|
|
||||||
dimension,
|
|
||||||
contextWindow
|
|
||||||
};
|
|
||||||
|
|
||||||
this.modelInfoCache.set(modelName, modelInfo);
|
|
||||||
this.config.dimension = dimension;
|
|
||||||
|
|
||||||
log.info(`Detected Anthropic model ${modelName} with dimension ${dimension} (context: ${contextWindow})`);
|
|
||||||
return modelInfo;
|
|
||||||
} catch (error: any) {
|
|
||||||
// If detection fails, use defaults
|
|
||||||
const dimension = LLM_CONSTANTS.EMBEDDING_DIMENSIONS.ANTHROPIC.DEFAULT;
|
|
||||||
|
|
||||||
log.info(`Using default parameters for Anthropic model ${modelName}: dimension ${dimension}, context ${contextWindow}`);
|
|
||||||
|
|
||||||
const modelInfo: EmbeddingModelInfo = { dimension, contextWindow };
|
|
||||||
this.modelInfoCache.set(modelName, modelInfo);
|
|
||||||
this.config.dimension = dimension;
|
|
||||||
|
|
||||||
return modelInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate embeddings for a single text
|
|
||||||
*/
|
|
||||||
async generateEmbeddings(text: string): Promise<Float32Array> {
|
|
||||||
try {
|
|
||||||
if (!text.trim()) {
|
|
||||||
return new Float32Array(this.config.dimension);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get model info to check context window
|
|
||||||
const modelName = this.config.model || "claude-3-haiku-20240307";
|
|
||||||
const modelInfo = await this.getModelInfo(modelName);
|
|
||||||
|
|
||||||
// Trim text if it might exceed context window (rough character estimate)
|
|
||||||
const charLimit = modelInfo.contextWindow * 4; // Rough estimate: avg 4 chars per token
|
|
||||||
const trimmedText = text.length > charLimit ? text.substring(0, charLimit) : text;
|
|
||||||
|
|
||||||
const response = await axios.post(
|
|
||||||
`${this.baseUrl}/embeddings`,
|
|
||||||
{
|
|
||||||
model: modelName,
|
|
||||||
input: trimmedText,
|
|
||||||
encoding_format: "float"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": this.apiKey,
|
|
||||||
"anthropic-version": "2023-06-01"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data && response.data.embedding) {
|
|
||||||
return new Float32Array(response.data.embedding);
|
|
||||||
} else {
|
|
||||||
throw new Error("Unexpected response structure from Anthropic API");
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMessage = error.response?.data?.error?.message || error.message || "Unknown error";
|
|
||||||
log.error(`Anthropic embedding error: ${errorMessage}`);
|
|
||||||
throw new Error(`Anthropic embedding error: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More specific implementation of batch size error detection for Anthropic
|
|
||||||
*/
|
|
||||||
protected isBatchSizeError(error: any): boolean {
|
|
||||||
const errorMessage = error?.message || error?.response?.data?.error?.message || '';
|
|
||||||
const anthropicBatchSizeErrorPatterns = [
|
|
||||||
'batch size', 'too many inputs', 'context length exceeded',
|
|
||||||
'token limit', 'rate limit', 'limit exceeded',
|
|
||||||
'too long', 'request too large', 'content too large'
|
|
||||||
];
|
|
||||||
|
|
||||||
return anthropicBatchSizeErrorPatterns.some(pattern =>
|
|
||||||
errorMessage.toLowerCase().includes(pattern.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate embeddings for multiple texts in a single batch
|
|
||||||
*
|
|
||||||
* Note: Anthropic doesn't currently support batch embedding, so we process each text individually
|
|
||||||
* but using the adaptive batch processor to handle errors and retries
|
|
||||||
*/
|
|
||||||
async generateBatchEmbeddings(texts: string[]): Promise<Float32Array[]> {
|
|
||||||
if (texts.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.processWithAdaptiveBatch(
|
|
||||||
texts,
|
|
||||||
async (batch) => {
|
|
||||||
const results: Float32Array[] = [];
|
|
||||||
|
|
||||||
// For Anthropic, we have to process one at a time
|
|
||||||
for (const text of batch) {
|
|
||||||
// Skip empty texts
|
|
||||||
if (!text.trim()) {
|
|
||||||
results.push(new Float32Array(this.config.dimension));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const embedding = await this.generateEmbeddings(text);
|
|
||||||
results.push(embedding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
this.isBatchSizeError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (error: any) {
|
|
||||||
const errorMessage = error.message || "Unknown error";
|
|
||||||
log.error(`Anthropic batch embedding error: ${errorMessage}`);
|
|
||||||
throw new Error(`Anthropic batch embedding error: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
254
src/services/llm/embeddings/providers/voyage.ts
Normal file
254
src/services/llm/embeddings/providers/voyage.ts
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import log from "../../../log.js";
|
||||||
|
import { BaseEmbeddingProvider } from "../base_embeddings.js";
|
||||||
|
import type { EmbeddingConfig, EmbeddingModelInfo } from "../embeddings_interface.js";
|
||||||
|
import { LLM_CONSTANTS } from "../../../../routes/api/llm.js";
|
||||||
|
|
||||||
|
// Voyage model context window sizes - as of current API version
|
||||||
|
const VOYAGE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
||||||
|
"voyage-large-2": 8192,
|
||||||
|
"voyage-2": 8192,
|
||||||
|
"default": 8192
|
||||||
|
};
|
||||||
|
|
||||||
|
// Voyage embedding dimensions
|
||||||
|
const VOYAGE_MODEL_DIMENSIONS: Record<string, number> = {
|
||||||
|
"voyage-large-2": 1536,
|
||||||
|
"voyage-2": 1024,
|
||||||
|
"default": 1024
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Voyage AI embedding provider implementation
|
||||||
|
*/
|
||||||
|
export class VoyageEmbeddingProvider extends BaseEmbeddingProvider {
|
||||||
|
name = "voyage";
|
||||||
|
|
||||||
|
constructor(config: EmbeddingConfig) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
// Set default base URL if not provided
|
||||||
|
if (!this.baseUrl) {
|
||||||
|
this.baseUrl = "https://api.voyageai.com/v1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the provider by detecting model capabilities
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
const modelName = this.config.model || "voyage-2";
|
||||||
|
try {
|
||||||
|
// Detect model capabilities
|
||||||
|
const modelInfo = await this.getModelInfo(modelName);
|
||||||
|
|
||||||
|
// Update the config dimension
|
||||||
|
this.config.dimension = modelInfo.dimension;
|
||||||
|
|
||||||
|
log.info(`Voyage AI model ${modelName} initialized with dimension ${this.config.dimension} and context window ${modelInfo.contextWindow}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error initializing Voyage AI provider: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to determine Voyage AI model capabilities
|
||||||
|
*/
|
||||||
|
private async fetchModelCapabilities(modelName: string): Promise<EmbeddingModelInfo | null> {
|
||||||
|
try {
|
||||||
|
// Get context window size from our local registry of known models
|
||||||
|
const modelBase = Object.keys(VOYAGE_MODEL_CONTEXT_WINDOWS).find(
|
||||||
|
model => modelName.startsWith(model)
|
||||||
|
) || "default";
|
||||||
|
|
||||||
|
const contextWindow = VOYAGE_MODEL_CONTEXT_WINDOWS[modelBase];
|
||||||
|
|
||||||
|
// Get dimension from our registry of known models
|
||||||
|
const dimension = VOYAGE_MODEL_DIMENSIONS[modelBase] || VOYAGE_MODEL_DIMENSIONS.default;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dimension,
|
||||||
|
contextWindow
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.info(`Could not determine capabilities for Voyage AI model ${modelName}: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model information including embedding dimensions
|
||||||
|
*/
|
||||||
|
async getModelInfo(modelName: string): Promise<EmbeddingModelInfo> {
|
||||||
|
// Check cache first
|
||||||
|
if (this.modelInfoCache.has(modelName)) {
|
||||||
|
return this.modelInfoCache.get(modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to determine model capabilities
|
||||||
|
const capabilities = await this.fetchModelCapabilities(modelName);
|
||||||
|
const contextWindow = capabilities?.contextWindow || 8192; // Default context window for Voyage
|
||||||
|
const knownDimension = capabilities?.dimension || 1024; // Default dimension for Voyage models
|
||||||
|
|
||||||
|
// For Voyage, we can use known dimensions or detect with a test call
|
||||||
|
try {
|
||||||
|
if (knownDimension) {
|
||||||
|
// Use known dimension
|
||||||
|
const modelInfo: EmbeddingModelInfo = {
|
||||||
|
dimension: knownDimension,
|
||||||
|
contextWindow
|
||||||
|
};
|
||||||
|
|
||||||
|
this.modelInfoCache.set(modelName, modelInfo);
|
||||||
|
this.config.dimension = knownDimension;
|
||||||
|
|
||||||
|
log.info(`Using known parameters for Voyage AI model ${modelName}: dimension ${knownDimension}, context ${contextWindow}`);
|
||||||
|
return modelInfo;
|
||||||
|
} else {
|
||||||
|
// Detect dimension with a test embedding as fallback
|
||||||
|
const testEmbedding = await this.generateEmbeddings("Test");
|
||||||
|
const dimension = testEmbedding.length;
|
||||||
|
|
||||||
|
const modelInfo: EmbeddingModelInfo = {
|
||||||
|
dimension,
|
||||||
|
contextWindow
|
||||||
|
};
|
||||||
|
|
||||||
|
this.modelInfoCache.set(modelName, modelInfo);
|
||||||
|
this.config.dimension = dimension;
|
||||||
|
|
||||||
|
log.info(`Detected Voyage AI model ${modelName} with dimension ${dimension} (context: ${contextWindow})`);
|
||||||
|
return modelInfo;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// If detection fails, use defaults
|
||||||
|
const dimension = 1024; // Default for Voyage models
|
||||||
|
|
||||||
|
log.info(`Using default parameters for Voyage AI model ${modelName}: dimension ${dimension}, context ${contextWindow}`);
|
||||||
|
|
||||||
|
const modelInfo: EmbeddingModelInfo = { dimension, contextWindow };
|
||||||
|
this.modelInfoCache.set(modelName, modelInfo);
|
||||||
|
this.config.dimension = dimension;
|
||||||
|
|
||||||
|
return modelInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate embeddings for a single text
|
||||||
|
*/
|
||||||
|
async generateEmbeddings(text: string): Promise<Float32Array> {
|
||||||
|
try {
|
||||||
|
if (!text.trim()) {
|
||||||
|
return new Float32Array(this.config.dimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get model info to check context window
|
||||||
|
const modelName = this.config.model || "voyage-2";
|
||||||
|
const modelInfo = await this.getModelInfo(modelName);
|
||||||
|
|
||||||
|
// Trim text if it might exceed context window (rough character estimate)
|
||||||
|
const charLimit = modelInfo.contextWindow * 4; // Rough estimate: avg 4 chars per token
|
||||||
|
const trimmedText = text.length > charLimit ? text.substring(0, charLimit) : text;
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseUrl}/embeddings`,
|
||||||
|
{
|
||||||
|
model: modelName,
|
||||||
|
input: trimmedText,
|
||||||
|
input_type: "text",
|
||||||
|
truncation: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${this.apiKey}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data && response.data.data && response.data.data[0] && response.data.data[0].embedding) {
|
||||||
|
return new Float32Array(response.data.data[0].embedding);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected response structure from Voyage AI API");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.error?.message || error.message || "Unknown error";
|
||||||
|
log.error(`Voyage AI embedding error: ${errorMessage}`);
|
||||||
|
throw new Error(`Voyage AI embedding error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* More specific implementation of batch size error detection for Voyage AI
|
||||||
|
*/
|
||||||
|
protected isBatchSizeError(error: any): boolean {
|
||||||
|
const errorMessage = error?.message || error?.response?.data?.error?.message || '';
|
||||||
|
const voyageBatchSizeErrorPatterns = [
|
||||||
|
'batch size', 'too many inputs', 'context length exceeded',
|
||||||
|
'token limit', 'rate limit', 'limit exceeded',
|
||||||
|
'too long', 'request too large', 'content too large'
|
||||||
|
];
|
||||||
|
|
||||||
|
return voyageBatchSizeErrorPatterns.some(pattern =>
|
||||||
|
errorMessage.toLowerCase().includes(pattern.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate embeddings for multiple texts in a single batch
|
||||||
|
*/
|
||||||
|
async generateBatchEmbeddings(texts: string[]): Promise<Float32Array[]> {
|
||||||
|
if (texts.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.processWithAdaptiveBatch(
|
||||||
|
texts,
|
||||||
|
async (batch) => {
|
||||||
|
if (batch.length === 0) return [];
|
||||||
|
if (batch.length === 1) {
|
||||||
|
return [await this.generateEmbeddings(batch[0])];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Voyage AI, we can batch embeddings
|
||||||
|
const modelName = this.config.model || "voyage-2";
|
||||||
|
|
||||||
|
// Filter out empty texts
|
||||||
|
const validBatch = batch.map(text => text.trim() || " ");
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseUrl}/embeddings`,
|
||||||
|
{
|
||||||
|
model: modelName,
|
||||||
|
input: validBatch,
|
||||||
|
input_type: "text",
|
||||||
|
truncation: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${this.apiKey}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data && response.data.data && Array.isArray(response.data.data)) {
|
||||||
|
return response.data.data.map((item: any) =>
|
||||||
|
new Float32Array(item.embedding || [])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected response structure from Voyage AI batch API");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.isBatchSizeError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
const errorMessage = error.message || "Unknown error";
|
||||||
|
log.error(`Voyage AI batch embedding error: ${errorMessage}`);
|
||||||
|
throw new Error(`Voyage AI batch embedding error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -176,7 +176,8 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
{ name: "openaiBaseUrl", value: "https://api.openai.com/v1", isSynced: true },
|
{ name: "openaiBaseUrl", value: "https://api.openai.com/v1", isSynced: true },
|
||||||
{ name: "anthropicApiKey", value: "", isSynced: false },
|
{ name: "anthropicApiKey", value: "", isSynced: false },
|
||||||
{ name: "anthropicDefaultModel", value: "claude-3-opus-20240229", isSynced: true },
|
{ name: "anthropicDefaultModel", value: "claude-3-opus-20240229", isSynced: true },
|
||||||
{ name: "anthropicEmbeddingModel", value: "", isSynced: true },
|
{ name: "voyageEmbeddingModel", value: "voyage-2", isSynced: true },
|
||||||
|
{ name: "voyageApiKey", value: "", isSynced: false },
|
||||||
{ name: "anthropicBaseUrl", value: "https://api.anthropic.com/v1", isSynced: true },
|
{ name: "anthropicBaseUrl", value: "https://api.anthropic.com/v1", isSynced: true },
|
||||||
{ name: "ollamaEnabled", value: "false", isSynced: true },
|
{ name: "ollamaEnabled", value: "false", isSynced: true },
|
||||||
{ name: "ollamaDefaultModel", value: "llama3", isSynced: true },
|
{ name: "ollamaDefaultModel", value: "llama3", isSynced: true },
|
||||||
@ -189,7 +190,7 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
{ name: "aiSystemPrompt", value: "", isSynced: true },
|
{ name: "aiSystemPrompt", value: "", isSynced: true },
|
||||||
{ name: "aiProviderPrecedence", value: "openai,anthropic,ollama", isSynced: true },
|
{ name: "aiProviderPrecedence", value: "openai,anthropic,ollama", isSynced: true },
|
||||||
{ name: "embeddingsDefaultProvider", value: "openai", isSynced: true },
|
{ name: "embeddingsDefaultProvider", value: "openai", isSynced: true },
|
||||||
{ name: "embeddingProviderPrecedence", value: "openai,ollama", isSynced: true },
|
{ name: "embeddingProviderPrecedence", value: "openai,voyage,ollama", isSynced: true },
|
||||||
{ name: "embeddingDimensionStrategy", value: "adapt", isSynced: true },
|
{ name: "embeddingDimensionStrategy", value: "adapt", isSynced: true },
|
||||||
{ name: "enableAutomaticIndexing", value: "true", isSynced: true },
|
{ name: "enableAutomaticIndexing", value: "true", isSynced: true },
|
||||||
{ name: "embeddingSimilarityThreshold", value: "0.65", isSynced: true },
|
{ name: "embeddingSimilarityThreshold", value: "0.65", isSynced: true },
|
||||||
|
@ -57,7 +57,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
|||||||
openaiBaseUrl: string;
|
openaiBaseUrl: string;
|
||||||
anthropicApiKey: string;
|
anthropicApiKey: string;
|
||||||
anthropicDefaultModel: string;
|
anthropicDefaultModel: string;
|
||||||
anthropicEmbeddingModel: string;
|
voyageEmbeddingModel: string;
|
||||||
|
voyageApiKey: string;
|
||||||
anthropicBaseUrl: string;
|
anthropicBaseUrl: string;
|
||||||
ollamaEnabled: boolean;
|
ollamaEnabled: boolean;
|
||||||
ollamaBaseUrl: string;
|
ollamaBaseUrl: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user