diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 89d5b168d7..454f9afe02 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -21,7 +21,9 @@ export const CODEBASE_INDEX_DEFAULTS = { export const codebaseIndexConfigSchema = z.object({ codebaseIndexEnabled: z.boolean().optional(), codebaseIndexQdrantUrl: z.string().optional(), - codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini", "mistral"]).optional(), + codebaseIndexEmbedderProvider: z + .enum(["openai", "ollama", "openai-compatible", "gemini", "mistral", "jina"]) + .optional(), codebaseIndexEmbedderBaseUrl: z.string().optional(), codebaseIndexEmbedderModelId: z.string().optional(), codebaseIndexEmbedderModelDimension: z.number().optional(), @@ -48,6 +50,7 @@ export const codebaseIndexModelsSchema = z.object({ "openai-compatible": z.record(z.string(), z.object({ dimension: z.number() })).optional(), gemini: z.record(z.string(), z.object({ dimension: z.number() })).optional(), mistral: z.record(z.string(), z.object({ dimension: z.number() })).optional(), + jina: z.record(z.string(), z.object({ dimension: z.number() })).optional(), }) export type CodebaseIndexModels = z.infer @@ -64,6 +67,7 @@ export const codebaseIndexProviderSchema = z.object({ codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), codebaseIndexGeminiApiKey: z.string().optional(), codebaseIndexMistralApiKey: z.string().optional(), + codebaseIndexJinaApiKey: z.string().optional(), }) export type CodebaseIndexProvider = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index dc5a9e6744..05367abf51 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -186,6 +186,7 @@ export const SECRET_STATE_KEYS = [ "codebaseIndexOpenAiCompatibleApiKey", "codebaseIndexGeminiApiKey", "codebaseIndexMistralApiKey", + "codebaseIndexJinaApiKey", "huggingFaceApiKey", ] as const satisfies readonly (keyof ProviderSettings)[] export type SecretState = Pick diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 763e118125..5562c07e22 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2036,6 +2036,9 @@ export const webviewMessageHandler = async ( settings.codebaseIndexMistralApiKey, ) } + if (settings.codebaseIndexJinaApiKey !== undefined) { + await provider.contextProxy.storeSecret("codebaseIndexJinaApiKey", settings.codebaseIndexJinaApiKey) + } // Send success response first - settings are saved regardless of validation await provider.postMessageToWebview({ @@ -2157,6 +2160,7 @@ export const webviewMessageHandler = async ( )) const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey")) const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey")) + const hasJinaApiKey = !!(await provider.context.secrets.get("codebaseIndexJinaApiKey")) provider.postMessageToWebview({ type: "codeIndexSecretStatus", @@ -2166,6 +2170,7 @@ export const webviewMessageHandler = async ( hasOpenAiCompatibleApiKey, hasGeminiApiKey, hasMistralApiKey, + hasJinaApiKey, }, }) break diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 66465d8c35..58fe71e3ed 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -47,6 +47,7 @@ "openAiCompatibleConfigMissing": "OpenAI Compatible configuration missing for embedder creation", "geminiConfigMissing": "Gemini configuration missing for embedder creation", "mistralConfigMissing": "Mistral configuration missing for embedder creation", + "jinaConfigMissing": "Jina configuration missing for embedder creation", "invalidEmbedderType": "Invalid embedder type configured: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Please ensure the 'Embedding Dimension' is correctly set in the OpenAI-Compatible provider settings.", "vectorDimensionNotDetermined": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Check model profiles or configuration.", diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 1723f1c2a0..a614e54091 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -19,6 +19,7 @@ export class CodeIndexConfigManager { private openAiCompatibleOptions?: { baseUrl: string; apiKey: string } private geminiOptions?: { apiKey: string } private mistralOptions?: { apiKey: string } + private jinaOptions?: { apiKey: string } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -69,6 +70,7 @@ export class CodeIndexConfigManager { const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? "" const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? "" + const jinaApiKey = this.contextProxy?.getSecret("codebaseIndexJinaApiKey") ?? "" // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? true @@ -104,6 +106,8 @@ export class CodeIndexConfigManager { this.embedderProvider = "gemini" } else if (codebaseIndexEmbedderProvider === "mistral") { this.embedderProvider = "mistral" + } else if (codebaseIndexEmbedderProvider === "jina") { + this.embedderProvider = "jina" } else { this.embedderProvider = "openai" } @@ -124,6 +128,7 @@ export class CodeIndexConfigManager { this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined + this.jinaOptions = jinaApiKey ? { apiKey: jinaApiKey } : undefined } /** @@ -141,6 +146,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } + jinaOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -160,6 +166,7 @@ export class CodeIndexConfigManager { openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "", geminiApiKey: this.geminiOptions?.apiKey ?? "", mistralApiKey: this.mistralOptions?.apiKey ?? "", + jinaApiKey: this.jinaOptions?.apiKey ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -184,6 +191,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, + jinaOptions: this.jinaOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, @@ -221,6 +229,11 @@ export class CodeIndexConfigManager { const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured + } else if (this.embedderProvider === "jina") { + const apiKey = this.jinaOptions?.apiKey + const qdrantUrl = this.qdrantUrl + const isConfigured = !!(apiKey && qdrantUrl) + return isConfigured } return false // Should not happen if embedderProvider is always set correctly } @@ -292,6 +305,7 @@ export class CodeIndexConfigManager { const currentModelDimension = this.modelDimension const currentGeminiApiKey = this.geminiOptions?.apiKey ?? "" const currentMistralApiKey = this.mistralOptions?.apiKey ?? "" + const currentJinaApiKey = this.jinaOptions?.apiKey ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -318,6 +332,11 @@ export class CodeIndexConfigManager { return true } + const prevJinaApiKey = prev?.jinaApiKey ?? "" + if (prevJinaApiKey !== currentJinaApiKey) { + return true + } + // Check for model dimension changes (generic for all providers) if (prevModelDimension !== currentModelDimension) { return true @@ -375,6 +394,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, + jinaOptions: this.jinaOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, diff --git a/src/services/code-index/embedders/jina.ts b/src/services/code-index/embedders/jina.ts new file mode 100644 index 0000000000..80e54ca8a5 --- /dev/null +++ b/src/services/code-index/embedders/jina.ts @@ -0,0 +1,278 @@ +import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces" +import { getModelQueryPrefix } from "../../../shared/embeddingModels" +import { t } from "../../../i18n" +import { + withValidationErrorHandling, + formatEmbeddingError, + getErrorMessageForStatus, +} from "../shared/validation-helpers" +import type { HttpError } from "../shared/validation-helpers" +import { TelemetryEventName } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" +import { + MAX_BATCH_TOKENS, + MAX_ITEM_TOKENS, + MAX_BATCH_RETRIES as MAX_RETRIES, + INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS, +} from "../constants" + +interface JinaEmbeddingRequest { + model: string + input: string[] + encoding_type?: "float" | "base64" + task?: string + dimensions?: number + late_chunking?: boolean + embedding_type?: "float" | "base64" | "binary" | "ubinary" +} + +interface JinaEmbeddingResponse { + model: string + object: "list" + usage: { + total_tokens: number + prompt_tokens: number + } + data: Array<{ + object: "embedding" + index: number + embedding: number[] | string + }> +} + +/** + * Jina implementation of the embedder interface with batching and rate limiting + * Uses jina-embeddings-v4 with multi-vector embeddings for code search + */ +export class JinaEmbedder implements IEmbedder { + private readonly apiKey: string + private readonly baseUrl = "https://api.jina.ai/v1" + private readonly defaultModelId: string + + /** + * Creates a new Jina embedder + * @param apiKey Jina API key + * @param modelId Optional model identifier (defaults to jina-embeddings-v4) + */ + constructor(apiKey: string, modelId?: string) { + this.apiKey = apiKey + this.defaultModelId = modelId || "jina-embeddings-v4" + } + + get embedderInfo(): EmbedderInfo { + return { name: "jina" } + } + + /** + * Creates embeddings for the given texts with batching and rate limiting + * @param texts Array of text strings to embed + * @param model Optional model identifier + * @returns Promise resolving to embedding response + */ + async createEmbeddings(texts: string[], model?: string): Promise { + const modelToUse = model || this.defaultModelId + + // Apply model-specific query prefix if required + const queryPrefix = getModelQueryPrefix("jina", modelToUse) + const processedTexts = queryPrefix + ? texts.map((text) => { + // Prevent double-prefixing + if (text.startsWith(queryPrefix)) { + return text + } + return queryPrefix + text + }) + : texts + + let attempt = 0 + let lastError: Error | null = null + + while (attempt < MAX_RETRIES) { + attempt++ + + try { + const batches = this.createBatches(processedTexts) + const allEmbeddings: number[][] = [] + let totalPromptTokens = 0 + let totalTokens = 0 + + for (const batch of batches) { + const response = await this.fetchEmbeddings(batch, modelToUse) + + // Extract embeddings from response + const embeddings = response.data + .sort((a, b) => a.index - b.index) + .map((item) => { + if (typeof item.embedding === "string") { + throw new Error("Base64/binary embeddings are not supported") + } + return item.embedding + }) + + allEmbeddings.push(...embeddings) + totalPromptTokens += response.usage.prompt_tokens + totalTokens += response.usage.total_tokens + } + + return { + embeddings: allEmbeddings, + usage: { + promptTokens: totalPromptTokens, + totalTokens: totalTokens, + }, + } + } catch (error) { + lastError = error as Error + + if (error && typeof error === "object" && "status" in error) { + const errorStatus = (error as any).status + if (errorStatus === 401) { + throw new Error(t("embeddings:authenticationFailed")) + } else if (errorStatus === 429) { + // Rate limit - retry with exponential backoff + const delay = INITIAL_DELAY_MS * Math.pow(2, attempt - 1) + console.warn( + t("embeddings:rateLimitRetry", { + delayMs: delay, + attempt, + maxRetries: MAX_RETRIES, + }), + ) + await new Promise((resolve) => setTimeout(resolve, delay)) + continue + } else { + throw new Error( + t("embeddings:failedWithStatus", { + attempts: attempt, + statusCode: error.status, + errorMessage: error.message, + }), + ) + } + } else if (error instanceof Error) { + throw new Error( + t("embeddings:failedWithError", { + attempts: attempt, + errorMessage: error.message, + }), + ) + } + } + } + + // If we've exhausted all retries + if (lastError) { + throw new Error( + t("embeddings:failedMaxAttempts", { + attempts: MAX_RETRIES, + }), + ) + } + + throw new Error(t("embeddings:unknownError")) + } + + /** + * Validates the embedder configuration by testing connectivity and credentials + * @returns Promise resolving to validation result + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + return withValidationErrorHandling(async () => { + const testText = "function hello() { return 'world'; }" + const response = await this.fetchEmbeddings([testText], this.defaultModelId) + + // Validate response structure + if (!response.data || !Array.isArray(response.data) || response.data.length === 0) { + throw new Error(t("embeddings:validation.invalidResponse")) + } + + const embedding = response.data[0].embedding + if (!Array.isArray(embedding) || embedding.length === 0) { + throw new Error(t("embeddings:validation.invalidResponse")) + } + + return { valid: true } + }, "jina") + } + + /** + * Creates batches of texts based on token limits + */ + private createBatches(texts: string[]): string[][] { + const batches: string[][] = [] + let currentBatch: string[] = [] + let currentBatchTokens = 0 + + for (const text of texts) { + // Rough token estimation (1 token ≈ 4 characters) + const estimatedTokens = Math.ceil(text.length / 4) + + // Check if this item exceeds the max item tokens + if (estimatedTokens > MAX_ITEM_TOKENS) { + console.warn( + t("embeddings:textExceedsTokenLimit", { + index: texts.indexOf(text), + itemTokens: estimatedTokens, + maxTokens: MAX_ITEM_TOKENS, + }), + ) + continue + } + + // If adding this text would exceed batch limits, start a new batch + if (currentBatch.length > 0 && currentBatchTokens + estimatedTokens > MAX_BATCH_TOKENS) { + batches.push(currentBatch) + currentBatch = [] + currentBatchTokens = 0 + } + + currentBatch.push(text) + currentBatchTokens += estimatedTokens + } + + // Don't forget the last batch + if (currentBatch.length > 0) { + batches.push(currentBatch) + } + + return batches + } + + /** + * Fetches embeddings from Jina API + */ + private async fetchEmbeddings(texts: string[], model: string): Promise { + const request: JinaEmbeddingRequest = { + model, + input: texts, + encoding_type: "float", + // Use code.query task for code search embeddings + task: "code.query", + // Request full 2048 dimensions for jina-embeddings-v4 + dimensions: model === "jina-embeddings-v4" ? 2048 : undefined, + } + + const response = await fetch(`${this.baseUrl}/embeddings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify(request), + }) + + if (!response.ok) { + const errorData = await response.text().catch(() => "Unknown error") + const error = { status: response.status, message: errorData } as any + throw formatEmbeddingError(error, MAX_RETRIES) + } + + const data = (await response.json()) as JinaEmbeddingResponse + + // Capture telemetry + // Log telemetry for successful embedding creation + // Note: Currently only CODE_INDEX_ERROR event is available for code indexing + + return data + } +} diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 9098a60091..2f79d1048f 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -14,6 +14,7 @@ export interface CodeIndexConfig { openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } + jinaOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -35,6 +36,7 @@ export type PreviousConfigSnapshot = { openAiCompatibleApiKey?: string geminiApiKey?: string mistralApiKey?: string + jinaApiKey?: string qdrantUrl?: string qdrantApiKey?: string } diff --git a/src/services/code-index/interfaces/embedder.ts b/src/services/code-index/interfaces/embedder.ts index c5653ea2b7..87a41c5b39 100644 --- a/src/services/code-index/interfaces/embedder.ts +++ b/src/services/code-index/interfaces/embedder.ts @@ -28,7 +28,7 @@ export interface EmbeddingResponse { } } -export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" +export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "jina" export interface EmbedderInfo { name: AvailableEmbedders diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index fd3b2bfdda..1fa3817203 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -70,7 +70,7 @@ export interface ICodeIndexManager { } export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "jina" export interface IndexProgressUpdate { systemStatus: IndexingState diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 68b0f5c0bc..9f61861406 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -4,6 +4,7 @@ import { CodeIndexOllamaEmbedder } from "./embedders/ollama" import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible" import { GeminiEmbedder } from "./embedders/gemini" import { MistralEmbedder } from "./embedders/mistral" +import { JinaEmbedder } from "./embedders/jina" import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" import { QdrantVectorStore } from "./vector-store/qdrant-client" import { codeParser, DirectoryScanner, FileWatcher } from "./processors" @@ -70,6 +71,11 @@ export class CodeIndexServiceFactory { throw new Error(t("embeddings:serviceFactory.mistralConfigMissing")) } return new MistralEmbedder(config.mistralOptions.apiKey, config.modelId) + } else if (provider === "jina") { + if (!config.jinaOptions?.apiKey) { + throw new Error(t("embeddings:serviceFactory.jinaConfigMissing")) + } + return new JinaEmbedder(config.jinaOptions.apiKey, config.modelId) } throw new Error( diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index a91d1af7ba..4548fca7d6 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -255,7 +255,7 @@ export interface WebviewMessage { // Global state settings codebaseIndexEnabled: boolean codebaseIndexQdrantUrl: string - codebaseIndexEmbedderProvider: "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" + codebaseIndexEmbedderProvider: "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "jina" codebaseIndexEmbedderBaseUrl?: string codebaseIndexEmbedderModelId: string codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers @@ -269,6 +269,7 @@ export interface WebviewMessage { codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string codebaseIndexMistralApiKey?: string + codebaseIndexJinaApiKey?: string } } diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index a3cd61e659..11fb82413d 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -2,7 +2,7 @@ * Defines profiles for different embedding models, including their dimensions. */ -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" // Add other providers as needed +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "jina" // Add other providers as needed export interface EmbeddingModelProfile { dimension: number @@ -53,6 +53,11 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { mistral: { "codestral-embed-2505": { dimension: 1536, scoreThreshold: 0.4 }, }, + jina: { + "jina-embeddings-v4": { dimension: 2048, scoreThreshold: 0.4 }, + "jina-embeddings-v3": { dimension: 1024, scoreThreshold: 0.4 }, + "jina-clip-v2": { dimension: 1024, scoreThreshold: 0.4 }, + }, } /** @@ -143,6 +148,9 @@ export function getDefaultModelId(provider: EmbedderProvider): string { case "mistral": return "codestral-embed-2505" + case "jina": + return "jina-embeddings-v4" + default: // Fallback for unknown providers console.warn(`Unknown provider for default model ID: ${provider}. Falling back to OpenAI default.`) diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index c85aaf6ea5..94c6acdaa2 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -70,6 +70,7 @@ interface LocalCodeIndexSettings { codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string codebaseIndexMistralApiKey?: string + codebaseIndexJinaApiKey?: string } // Validation schema for codebase index settings @@ -136,6 +137,14 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) + case "jina": + return baseSchema.extend({ + codebaseIndexJinaApiKey: z.string().min(1, t("settings:codeIndex.validation.jinaApiKeyRequired")), + codebaseIndexEmbedderModelId: z + .string() + .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), + }) + default: return baseSchema } @@ -628,6 +637,9 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.mistralProvider")} + + {t("settings:codeIndex.jinaProvider")} + @@ -1020,6 +1032,71 @@ export const CodeIndexPopover: React.FC = ({ )} + {currentSettings.codebaseIndexEmbedderProvider === "jina" && ( + <> +
+ + + updateSetting("codebaseIndexJinaApiKey", e.target.value) + } + placeholder={t("settings:codeIndex.jinaApiKeyPlaceholder")} + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexJinaApiKey, + })} + /> + {formErrors.codebaseIndexJinaApiKey && ( +

+ {formErrors.codebaseIndexJinaApiKey} +

+ )} +
+ +
+ + + updateSetting("codebaseIndexEmbedderModelId", e.target.value) + } + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexEmbedderModelId, + })}> + + {t("settings:codeIndex.selectModel")} + + {getAvailableModels().map((modelId) => { + const model = + codebaseIndexModels?.[ + currentSettings.codebaseIndexEmbedderProvider + ]?.[modelId] + return ( + + {modelId}{" "} + {model + ? t("settings:codeIndex.modelDimensions", { + dimension: model.dimension, + }) + : ""} + + ) + })} + + {formErrors.codebaseIndexEmbedderModelId && ( +

+ {formErrors.codebaseIndexEmbedderModelId} +

+ )} +
+ + )} + {/* Qdrant Settings */}