Add Together embedder to TS SDK (#5989)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Jaco-Ren
2026-07-06 20:24:14 +05:30
committed by GitHub
co-authored by kartik-mem0
parent b0bee551cb
commit 4c974c8fa8
10 changed files with 233 additions and 24 deletions
@@ -0,0 +1,24 @@
import { OpenAIEmbedder } from "./openai";
import { EmbeddingConfig } from "../types";
const DEFAULT_BASE_URL = "https://api.together.ai/v1";
const DEFAULT_MODEL = "intfloat/multilingual-e5-large-instruct";
export class TogetherEmbedder extends OpenAIEmbedder {
constructor(config: EmbeddingConfig) {
const openAICompatibleConfig = { ...config };
delete openAICompatibleConfig.embeddingDims;
const apiKey = config.apiKey || process.env.TOGETHER_API_KEY;
if (!apiKey) {
throw new Error("Together API key is required");
}
super({
...openAICompatibleConfig,
apiKey,
baseURL: config.baseURL || config.url || DEFAULT_BASE_URL,
model: config.model || DEFAULT_MODEL,
});
}
}
+1
View File
@@ -5,6 +5,7 @@ export * from "./embeddings/base";
export * from "./embeddings/openai";
export * from "./embeddings/ollama";
export * from "./embeddings/lmstudio";
export * from "./embeddings/together";
export * from "./embeddings/google";
export * from "./embeddings/azure";
export * from "./embeddings/langchain";
+3
View File
@@ -1,6 +1,7 @@
import { OpenAIEmbedder } from "../embeddings/openai";
import { OllamaEmbedder } from "../embeddings/ollama";
import { LMStudioEmbedder } from "../embeddings/lmstudio";
import { TogetherEmbedder } from "../embeddings/together";
import { OpenAILLM } from "../llms/openai";
import { OpenAIStructuredLLM } from "../llms/openai_structured";
import { AnthropicLLM } from "../llms/anthropic";
@@ -51,6 +52,8 @@ export class EmbedderFactory {
return new OllamaEmbedder(config);
case "lmstudio":
return new LMStudioEmbedder(config);
case "together":
return new TogetherEmbedder(config);
case "google":
case "gemini":
return new GoogleEmbedder(config);
@@ -35,6 +35,11 @@ jest.mock("../src/embeddings/lmstudio", () => ({
.fn()
.mockImplementation((config) => ({ type: "lmstudio-embedder", config })),
}));
jest.mock("../src/embeddings/together", () => ({
TogetherEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "together-embedder", config })),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest
@@ -190,6 +195,7 @@ describe("EmbedderFactory", () => {
["azure_openai"],
["langchain"],
["lmstudio"],
["together"],
])("creates embedder for provider '%s'", (provider) => {
expect(() =>
EmbedderFactory.create(provider, dummyEmbedConfig),
@@ -0,0 +1,119 @@
/// <reference types="jest" />
const mockEmbeddingsCreate = jest.fn();
const mockOpenAI = jest.fn().mockImplementation(() => ({
embeddings: { create: mockEmbeddingsCreate },
}));
jest.mock("openai", () => ({
__esModule: true,
default: mockOpenAI,
}));
import { TogetherEmbedder } from "../src/embeddings/together";
const mockEmbedding = [0.1, 0.2, 0.3];
const originalEnv = process.env;
describe("TogetherEmbedder (unit)", () => {
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.TOGETHER_API_KEY;
mockOpenAI.mockClear();
mockEmbeddingsCreate.mockReset();
mockEmbeddingsCreate.mockResolvedValue({
data: [{ index: 0, embedding: mockEmbedding }],
});
});
afterAll(() => {
process.env = originalEnv;
});
it("uses Together defaults with an API key from config", async () => {
const embedder = new TogetherEmbedder({ apiKey: "test-key" });
await embedder.embed("hello");
expect(mockOpenAI).toHaveBeenCalledWith({
apiKey: "test-key",
baseURL: "https://api.together.ai/v1",
});
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
model: "intfloat/multilingual-e5-large-instruct",
input: "hello",
encoding_format: "float",
});
});
it("uses TOGETHER_API_KEY when config apiKey is not provided", async () => {
process.env.TOGETHER_API_KEY = "env-key";
const embedder = new TogetherEmbedder({});
await embedder.embed("hello");
expect(mockOpenAI).toHaveBeenCalledWith({
apiKey: "env-key",
baseURL: "https://api.together.ai/v1",
});
});
it("supports custom model and baseURL without forwarding embeddingDims", async () => {
const embedder = new TogetherEmbedder({
apiKey: "test-key",
model: "custom-together-embed",
baseURL: "https://proxy.example.com/v1",
embeddingDims: 512,
});
await embedder.embed("hello");
expect(mockOpenAI).toHaveBeenCalledWith({
apiKey: "test-key",
baseURL: "https://proxy.example.com/v1",
});
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
model: "custom-together-embed",
input: "hello",
encoding_format: "float",
});
});
it("uses url as a baseURL fallback", async () => {
const embedder = new TogetherEmbedder({
apiKey: "test-key",
url: "https://url-fallback.example.com/v1",
});
await embedder.embed("hello");
expect(mockOpenAI).toHaveBeenCalledWith({
apiKey: "test-key",
baseURL: "https://url-fallback.example.com/v1",
});
});
it("sorts batch embeddings by response index", async () => {
mockEmbeddingsCreate.mockResolvedValueOnce({
data: [
{ index: 1, embedding: [0.3, 0.4] },
{ index: 0, embedding: [0.1, 0.2] },
],
});
const embedder = new TogetherEmbedder({ apiKey: "test-key" });
await expect(embedder.embedBatch(["first", "second"])).resolves.toEqual([
[0.1, 0.2],
[0.3, 0.4],
]);
});
it("throws when no API key is available", () => {
expect(() => new TogetherEmbedder({})).toThrow(
"Together API key is required",
);
});
});