import type { EmbeddingConfig } from "./types.js";
export interface EmbeddingService {
embed(texts: string[], config: EmbeddingConfig): Promise<number[][]>;
}
export class OpenAICompatibleEmbeddingService implements EmbeddingService {
async embed(texts: string[], config: EmbeddingConfig): Promise<number[][]> {
if (!config.api_key) {
throw new Error("EMBEDDING_API_KEY_REQUIRED");
}
const response = await fetch(`${config.base_url.replace(/\/$/u, "")}/embeddings`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.api_key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ input: texts, model: config.model })
});
if (!response.ok) {
const detail = (await response.text()).slice(0, 500);
throw new Error(`EMBEDDING_REQUEST_FAILED:${response.status}:${detail}`);
}
const payload: unknown = await response.json();
if (!isRecord(payload) || !Array.isArray(payload.data)) {
throw new Error("EMBEDDING_RESPONSE_INVALID");
}
return payload.data.filter(isRecord).sort((left, right) => numberValue(left.index) - numberValue(right.index))
.map((item) => parseVector(item.embedding));
}
}
export const parseVector = (value: unknown): number[] => {
const parsed = typeof value === "string" ? JSON.parse(value) as unknown : value;
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every((item) => typeof item === "number")) {
throw new Error("EMBEDDING_VECTOR_INVALID");
}
return parsed;
};
export const cosineSimilarity = (left: number[], right: number[]): number => {
if (left.length !== right.length) {
throw new Error("EMBEDDING_DIMENSION_MISMATCH");
}
let dot = 0;
let leftNorm = 0;
let rightNorm = 0;
left.forEach((value, index) => {
const other = right[index] ?? 0;
dot += value * other;
leftNorm += value * value;
rightNorm += other * other;
});
return leftNorm > 0 && rightNorm > 0 ? dot / Math.sqrt(leftNorm * rightNorm) : 0;
};
const numberValue = (value: unknown): number => typeof value === "number" && Number.isFinite(value) ? value : 0;
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;