chore(deps): drop @langchain/community, hand-roll Jina embeddings

Replaces the single use of @langchain/community (JinaEmbeddings) with a small
hand-rolled implementation that extends @langchain/core/embeddings directly and
calls the Jina /embeddings endpoint via Obsidian's requestUrl (CORS-safe). The
request shape and response handling mirror the upstream Python reference.
This commit is contained in:
Zero Liu 2026-05-15 00:33:41 -07:00
parent adf8c68a73
commit 734ef8ea4f
No known key found for this signature in database
3 changed files with 138 additions and 1549 deletions

1603
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -78,7 +78,6 @@
"@dnd-kit/utilities": "^3.2.2",
"@langchain/anthropic": "^1.0.0",
"@langchain/classic": "^1.0.9",
"@langchain/community": "^1.0.0",
"@langchain/core": "^1.1.29",
"@langchain/deepseek": "^1.0.0",
"@langchain/google-genai": "^2.1.23",

View file

@ -1,15 +1,78 @@
import { JinaEmbeddings, JinaEmbeddingsParams } from "@langchain/community/embeddings/jina";
import { Embeddings, EmbeddingsParams } from "@langchain/core/embeddings";
import { requestUrl } from "obsidian";
export class CustomJinaEmbeddings extends JinaEmbeddings {
constructor(
fields?: Partial<JinaEmbeddingsParams> & {
apiKey?: string;
baseUrl?: string;
const DEFAULT_JINA_API_URL = "https://api.jina.ai/v1/embeddings";
interface JinaEmbeddingsConfig extends EmbeddingsParams {
model?: string;
modelName?: string;
apiKey?: string;
baseUrl?: string;
dimensions?: number;
}
interface JinaEmbeddingItem {
index: number;
embedding: number[];
}
interface JinaEmbeddingResponse {
data?: JinaEmbeddingItem[];
detail?: unknown;
}
export class CustomJinaEmbeddings extends Embeddings {
private readonly model: string;
private readonly apiKey: string;
private readonly url: string;
private readonly dimensions?: number;
constructor(config: JinaEmbeddingsConfig = {}) {
super(config);
this.model = config.model ?? config.modelName ?? "jina-embeddings-v2-base-en";
this.apiKey = config.apiKey ?? "";
this.url = config.baseUrl ?? DEFAULT_JINA_API_URL;
this.dimensions = config.dimensions;
}
async embedQuery(text: string): Promise<number[]> {
const embeddings = await this._embed([text]);
return embeddings[0];
}
async embedDocuments(texts: string[]): Promise<number[][]> {
return this._embed(texts);
}
private async _embed(input: string[]): Promise<number[][]> {
const body: Record<string, unknown> = { input, model: this.model };
if (this.dimensions !== undefined) {
body.dimensions = this.dimensions;
}
) {
super(fields);
if (fields?.baseUrl) {
this.baseUrl = fields.baseUrl;
const response = await requestUrl({
url: this.url,
method: "POST",
contentType: "application/json",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Accept-Encoding": "identity",
},
body: JSON.stringify(body),
throw: false,
});
if (response.status >= 400) {
throw new Error(`Jina embedding request failed: ${response.status} - ${response.text ?? ""}`);
}
const resp = response.json as JinaEmbeddingResponse;
if (!resp?.data) {
throw new Error(
typeof resp?.detail === "string" ? resp.detail : JSON.stringify(resp?.detail ?? resp)
);
}
return [...resp.data].sort((a, b) => a.index - b.index).map((item) => item.embedding);
}
}