mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
chore(deps): drop @langchain/community + bump openai to v6 to dedupe SDKs (#2463)
* 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. * chore(jina): adapt upstream community implementation with attribution Restores batching, dimensions/normalization defaults, and the multi-modal input type from the original @langchain/community JinaEmbeddings, with upstream copyright notice and MIT attribution preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): bump openai to ^6.10.0 to dedupe with @langchain/openai @langchain/openai@1 pins openai@^6.10.0 while we pinned ^4.95.1, so esbuild was shipping both copies. Aligning to v6 dedupes the bundle. Stacks on #2461, which drops @langchain/community (and its stagehand peer that pinned openai@^4.62.1), so npm install no longer needs --legacy-peer-deps to resolve. ChatOpenRouter.ts is the only direct consumer; its imports (OpenAI default, ChatCompletionChunk/MessageParam/Role types, chat.completions.create streaming) are all unchanged in v6. main.js: 3,447,540 -> 3,371,972 bytes (-75 KB). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
adf8c68a73
commit
0c9ce57dd5
3 changed files with 249 additions and 1589 deletions
1652
package-lock.json
generated
1652
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
@ -113,7 +112,7 @@
|
|||
"lucide-react": "^0.462.0",
|
||||
"luxon": "^3.5.0",
|
||||
"minisearch": "^7.2.0",
|
||||
"openai": "^4.95.1",
|
||||
"openai": "^6.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,186 @@
|
|||
import { JinaEmbeddings, JinaEmbeddingsParams } from "@langchain/community/embeddings/jina";
|
||||
/*
|
||||
* Adapted from @langchain/community JinaEmbeddings.
|
||||
* Copyright (c) LangChain, Inc. Licensed under the MIT License.
|
||||
* Source: https://github.com/langchain-ai/langchainjs-community/blob/886df5749a926f59e6fdf38a3465c62ec9e7ce32/libs/community/src/embeddings/jina.ts
|
||||
*/
|
||||
|
||||
export class CustomJinaEmbeddings extends JinaEmbeddings {
|
||||
import { Embeddings, type EmbeddingsParams } from "@langchain/core/embeddings";
|
||||
import { chunkArray } from "@langchain/core/utils/chunk_array";
|
||||
import { getEnvironmentVariable } from "@langchain/core/utils/env";
|
||||
|
||||
export interface JinaEmbeddingsParams extends EmbeddingsParams {
|
||||
/** Model name to use. */
|
||||
model: string;
|
||||
/** Compatibility alias used by this plugin's embedding manager. */
|
||||
modelName?: string;
|
||||
/** Jina-compatible embeddings endpoint. */
|
||||
baseUrl?: string;
|
||||
/** Timeout to use when making requests to Jina. */
|
||||
timeout?: number;
|
||||
/** The maximum number of documents to embed in a single request. */
|
||||
batchSize?: number;
|
||||
/** Whether to strip new lines from the input text. */
|
||||
stripNewLines?: boolean;
|
||||
/** The dimensions of the embedding. */
|
||||
dimensions?: number;
|
||||
/** Whether to L2-normalize the embedding vectors. */
|
||||
normalized?: boolean;
|
||||
}
|
||||
|
||||
type JinaMultiModelInput =
|
||||
| {
|
||||
text: string;
|
||||
image?: never;
|
||||
}
|
||||
| {
|
||||
image: string;
|
||||
text?: never;
|
||||
};
|
||||
|
||||
export type JinaEmbeddingsInput = string | JinaMultiModelInput;
|
||||
|
||||
interface EmbeddingCreateParams {
|
||||
model: JinaEmbeddingsParams["model"];
|
||||
input: JinaEmbeddingsInput[];
|
||||
dimensions: number;
|
||||
task: "retrieval.query" | "retrieval.passage";
|
||||
normalized?: boolean;
|
||||
}
|
||||
|
||||
interface EmbeddingResponse {
|
||||
model: string;
|
||||
object: string;
|
||||
usage: {
|
||||
total_tokens: number;
|
||||
prompt_tokens: number;
|
||||
};
|
||||
data: {
|
||||
object: string;
|
||||
index: number;
|
||||
embedding: number[];
|
||||
}[];
|
||||
}
|
||||
|
||||
interface EmbeddingErrorResponse {
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export class CustomJinaEmbeddings extends Embeddings implements JinaEmbeddingsParams {
|
||||
model: JinaEmbeddingsParams["model"] = "jina-clip-v2";
|
||||
batchSize = 24;
|
||||
baseUrl = "https://api.jina.ai/v1/embeddings";
|
||||
stripNewLines = true;
|
||||
dimensions = 1024;
|
||||
apiKey: string;
|
||||
normalized = true;
|
||||
|
||||
/**
|
||||
* Creates a Jina embeddings client using local configuration or Jina environment variables.
|
||||
*/
|
||||
constructor(
|
||||
fields?: Partial<JinaEmbeddingsParams> & {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
) {
|
||||
super(fields);
|
||||
if (fields?.baseUrl) {
|
||||
this.baseUrl = fields.baseUrl;
|
||||
const fieldsWithDefaults = { maxConcurrency: 2, ...fields };
|
||||
super(fieldsWithDefaults);
|
||||
|
||||
const apiKey =
|
||||
fieldsWithDefaults?.apiKey ||
|
||||
getEnvironmentVariable("JINA_API_KEY") ||
|
||||
getEnvironmentVariable("JINA_AUTH_TOKEN");
|
||||
|
||||
if (!apiKey) throw new Error("Jina API key not found");
|
||||
|
||||
this.apiKey = apiKey;
|
||||
this.model = fieldsWithDefaults?.model ?? fieldsWithDefaults?.modelName ?? this.model;
|
||||
this.baseUrl = fieldsWithDefaults?.baseUrl ?? this.baseUrl;
|
||||
this.dimensions = fieldsWithDefaults?.dimensions ?? this.dimensions;
|
||||
this.batchSize = fieldsWithDefaults?.batchSize ?? this.batchSize;
|
||||
this.stripNewLines = fieldsWithDefaults?.stripNewLines ?? this.stripNewLines;
|
||||
this.normalized = fieldsWithDefaults?.normalized ?? this.normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds passage documents with Jina retrieval-passage task parameters.
|
||||
*/
|
||||
async embedDocuments(input: JinaEmbeddingsInput[]): Promise<number[][]> {
|
||||
const batches = chunkArray(this.doStripNewLines(input), this.batchSize);
|
||||
const batchRequests = batches.map((batch) => {
|
||||
const params = this.getParams(batch);
|
||||
return this.embeddingWithRetry(params);
|
||||
});
|
||||
|
||||
const batchResponses = await Promise.all(batchRequests);
|
||||
const embeddings: number[][] = [];
|
||||
|
||||
for (let i = 0; i < batchResponses.length; i += 1) {
|
||||
const batch = batches[i];
|
||||
const batchResponse = batchResponses[i] || [];
|
||||
for (let j = 0; j < batch.length; j += 1) {
|
||||
embeddings.push(batchResponse[j]);
|
||||
}
|
||||
}
|
||||
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a query with Jina retrieval-query task parameters.
|
||||
*/
|
||||
async embedQuery(input: JinaEmbeddingsInput): Promise<number[]> {
|
||||
const params = this.getParams(this.doStripNewLines([input]), true);
|
||||
const embeddings = (await this.embeddingWithRetry(params)) || [[]];
|
||||
return embeddings[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes newlines from string inputs when configured to match upstream Jina behavior.
|
||||
*/
|
||||
private doStripNewLines(input: JinaEmbeddingsInput[]): JinaEmbeddingsInput[] {
|
||||
if (this.stripNewLines) {
|
||||
return input.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return item.replace(/\n/g, " ");
|
||||
}
|
||||
if (item.text) {
|
||||
return { text: item.text.replace(/\n/g, " ") };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the request body for Jina's retrieval embedding API.
|
||||
*/
|
||||
private getParams(input: JinaEmbeddingsInput[], query?: boolean): EmbeddingCreateParams {
|
||||
return {
|
||||
model: this.model,
|
||||
input,
|
||||
dimensions: this.dimensions,
|
||||
task: query ? "retrieval.query" : "retrieval.passage",
|
||||
normalized: this.normalized,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a single embeddings request and returns vectors in response order.
|
||||
*/
|
||||
private async embeddingWithRetry(body: EmbeddingCreateParams): Promise<number[][]> {
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const embeddingData: EmbeddingResponse | EmbeddingErrorResponse = await response.json();
|
||||
if ("detail" in embeddingData && embeddingData.detail) {
|
||||
throw new Error(`${embeddingData.detail}`);
|
||||
}
|
||||
return (embeddingData as EmbeddingResponse).data.map(({ embedding }) => embedding);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue