mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
feat(llm): Added Groq and Deepseek providers
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
AnthropicProvider,
|
||||
OpenAIProvider,
|
||||
OpenAIEmbeddingProvider,
|
||||
GroqProvider,
|
||||
DeepSeekProvider,
|
||||
GeminiEmbeddingProvider,
|
||||
MockEmbeddingProvider,
|
||||
} from "@omnia/llm";
|
||||
@@ -84,6 +86,20 @@ function buildLLMProvider(inst: ModelProviderInstance): ILLMProvider {
|
||||
inst.name,
|
||||
inst.maxContext,
|
||||
);
|
||||
} else if (inst.providerName === "groq") {
|
||||
return new GroqProvider(
|
||||
inst.apiKey,
|
||||
inst.modelName,
|
||||
inst.name,
|
||||
inst.maxContext,
|
||||
);
|
||||
} else if (inst.providerName === "deepseek") {
|
||||
return new DeepSeekProvider(
|
||||
inst.apiKey,
|
||||
inst.modelName,
|
||||
inst.name,
|
||||
inst.maxContext,
|
||||
);
|
||||
}
|
||||
return new MockLLMProvider([]);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^0.3.11",
|
||||
"@langchain/deepseek": "^1.1.5",
|
||||
"@langchain/google-genai": "^2.2.0",
|
||||
"@langchain/groq": "^1.3.1",
|
||||
"@langchain/ollama": "^0.2.3",
|
||||
"@langchain/openai": "^0.3.17",
|
||||
"@langchain/openrouter": "^0.4.3",
|
||||
|
||||
@@ -279,6 +279,48 @@ Also exports `GeminiEmbeddingProvider` (implements `IEmbeddingProvider`) using t
|
||||
4. None found → throw Error
|
||||
```
|
||||
|
||||
### Groq — `GroqProvider`
|
||||
|
||||
| Property | Value |
|
||||
| --------------------------- | -------------------------------------------- |
|
||||
| **File** | [`providers/groq.ts`](src/providers/groq.ts) |
|
||||
| **Provider ID** | `groq` |
|
||||
| **SDK** | `@langchain/groq` (`ChatGroq`) |
|
||||
| **Default Model** | `llama-3.3-70b-versatile` |
|
||||
| **Default Embedding Model** | _(none)_ |
|
||||
| **Default Max Context** | `8192` |
|
||||
| **Type** | Generative only (no embedding provider) |
|
||||
|
||||
**Key resolution** in the constructor follows this cascade:
|
||||
|
||||
```
|
||||
1. Explicit apiKey argument → use it
|
||||
2. ProviderManager.getActive() → if providerName matches "groq"
|
||||
3. GROQ_API_KEY env var → final fallback
|
||||
4. None found → throw Error
|
||||
```
|
||||
|
||||
### DeepSeek — `DeepSeekProvider`
|
||||
|
||||
| Property | Value |
|
||||
| --------------------------- | ---------------------------------------------------- |
|
||||
| **File** | [`providers/deepseek.ts`](src/providers/deepseek.ts) |
|
||||
| **Provider ID** | `deepseek` |
|
||||
| **SDK** | `@langchain/deepseek` (`ChatDeepSeek`) |
|
||||
| **Default Model** | `deepseek-chat` |
|
||||
| **Default Embedding Model** | _(none)_ |
|
||||
| **Default Max Context** | `64000` |
|
||||
| **Type** | Generative only (no embedding provider) |
|
||||
|
||||
**Key resolution** in the constructor follows this cascade:
|
||||
|
||||
```
|
||||
1. Explicit apiKey argument → use it
|
||||
2. ProviderManager.getActive() → if providerName matches "deepseek"
|
||||
3. DEEPSEEK_API_KEY env var → final fallback
|
||||
4. None found → throw Error
|
||||
```
|
||||
|
||||
### OpenAI — `OpenAIProvider`
|
||||
|
||||
| Property | Value |
|
||||
@@ -404,6 +446,8 @@ The `buildLLMProvider()` and `buildEmbeddingProvider()` functions perform the fi
|
||||
| `"openrouter"` | `OpenRouterProvider` | _(falls through to mock)_ |
|
||||
| `"ollama"` | `OllamaProvider` | `OllamaEmbeddingProvider` |
|
||||
| `"anthropic"` | `AnthropicProvider` | _(falls through to mock)_ |
|
||||
| `"groq"` | `GroqProvider` | _(falls through to mock)_ |
|
||||
| `"deepseek"` | `DeepSeekProvider` | _(falls through to mock)_ |
|
||||
| _(anything else)_ | `MockLLMProvider` | `MockEmbeddingProvider` |
|
||||
|
||||
## Structured Output
|
||||
@@ -432,6 +476,8 @@ This sends the Zod schema to the model as a structured output constraint. The re
|
||||
| `OPENAI_API_KEY` | No | OpenAI API key |
|
||||
| `OPENROUTER_API_KEY` | No | OpenRouter API key |
|
||||
| `ANTHROPIC_API_KEY` | No | Anthropic Claude API key |
|
||||
| `GROQ_API_KEY` | No | Groq API key |
|
||||
| `DEEPSEEK_API_KEY` | No | DeepSeek API key |
|
||||
|
||||
Both are optional because providers can also be configured through the database via the GUI settings page.
|
||||
|
||||
@@ -450,6 +496,8 @@ packages/llm/
|
||||
│ ├── openrouter.ts # OpenRouterProvider
|
||||
│ ├── anthropic.ts # AnthropicProvider
|
||||
│ ├── openai.ts # OpenAIProvider + OpenAIEmbeddingProvider
|
||||
│ ├── groq.ts # GroqProvider
|
||||
│ ├── deepseek.ts # DeepSeekProvider
|
||||
│ └── mock.ts # MockLLMProvider + MockEmbeddingProvider
|
||||
├── tests/
|
||||
│ ├── mock.test.ts
|
||||
|
||||
@@ -5,6 +5,8 @@ const LLMConfigSchema = z.object({
|
||||
OPENROUTER_API_KEY: z.string().optional(),
|
||||
ANTHROPIC_API_KEY: z.string().optional(),
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
GROQ_API_KEY: z.string().optional(),
|
||||
DEEPSEEK_API_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const llmConfig = LLMConfigSchema.parse(process.env);
|
||||
|
||||
@@ -6,4 +6,6 @@ export * from "./providers/ollama.js";
|
||||
export * from "./providers/openrouter.js";
|
||||
export * from "./providers/anthropic.js";
|
||||
export * from "./providers/openai.js";
|
||||
export * from "./providers/groq.js";
|
||||
export * from "./providers/deepseek.js";
|
||||
export * from "./provider-manager.js";
|
||||
|
||||
@@ -90,6 +90,20 @@ export const AVAILABLE_PROVIDERS: ModelProviderMeta[] = [
|
||||
defaultModel: "claude-3-5-sonnet-latest",
|
||||
defaultEmbeddingModel: "",
|
||||
},
|
||||
{
|
||||
id: "groq",
|
||||
displayName: "Groq",
|
||||
description: "Official Groq integration using @langchain/groq SDK",
|
||||
defaultModel: "llama-3.3-70b-versatile",
|
||||
defaultEmbeddingModel: "",
|
||||
},
|
||||
{
|
||||
id: "deepseek",
|
||||
displayName: "DeepSeek",
|
||||
description: "Official DeepSeek integration using @langchain/deepseek SDK",
|
||||
defaultModel: "deepseek-chat",
|
||||
defaultEmbeddingModel: "",
|
||||
},
|
||||
{
|
||||
id: "openrouter",
|
||||
displayName: "OpenRouter",
|
||||
|
||||
@@ -101,6 +101,8 @@ function getSettingsDb() {
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
const groqKey = process.env.GROQ_API_KEY;
|
||||
const deepseekKey = process.env.DEEPSEEK_API_KEY;
|
||||
let hasInsertedGenerative = false;
|
||||
let hasInsertedEmbedding = false;
|
||||
|
||||
@@ -209,6 +211,52 @@ function getSettingsDb() {
|
||||
}
|
||||
}
|
||||
|
||||
if (groqKey && groqKey.trim()) {
|
||||
const id = "provider-default-groq";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"Groq (Env)",
|
||||
"groq",
|
||||
groqKey.trim(),
|
||||
isActive,
|
||||
"llama-3.3-70b-versatile",
|
||||
"generative",
|
||||
8192,
|
||||
);
|
||||
if (isActive === 1) {
|
||||
hasInsertedGenerative = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (deepseekKey && deepseekKey.trim()) {
|
||||
const id = "provider-default-deepseek";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"DeepSeek (Env)",
|
||||
"deepseek",
|
||||
deepseekKey.trim(),
|
||||
isActive,
|
||||
"deepseek-chat",
|
||||
"generative",
|
||||
64000,
|
||||
);
|
||||
if (isActive === 1) {
|
||||
hasInsertedGenerative = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
@@ -464,6 +512,8 @@ export class ProviderManager {
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
const groqKey = process.env.GROQ_API_KEY;
|
||||
const deepseekKey = process.env.DEEPSEEK_API_KEY;
|
||||
let hasInsertedGenerative = false;
|
||||
let hasInsertedEmbedding = false;
|
||||
|
||||
@@ -572,6 +622,52 @@ export class ProviderManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (groqKey && groqKey.trim()) {
|
||||
const id = "provider-default-groq";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"Groq (Env)",
|
||||
"groq",
|
||||
groqKey.trim(),
|
||||
isActive,
|
||||
"llama-3.3-70b-versatile",
|
||||
"generative",
|
||||
8192,
|
||||
);
|
||||
if (isActive === 1) {
|
||||
hasInsertedGenerative = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (deepseekKey && deepseekKey.trim()) {
|
||||
const id = "provider-default-deepseek";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"DeepSeek (Env)",
|
||||
"deepseek",
|
||||
deepseekKey.trim(),
|
||||
isActive,
|
||||
"deepseek-chat",
|
||||
"generative",
|
||||
64000,
|
||||
);
|
||||
if (isActive === 1) {
|
||||
hasInsertedGenerative = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
@@ -757,6 +853,32 @@ export class ProviderManager {
|
||||
maxContext: 200000,
|
||||
};
|
||||
}
|
||||
const groqKey = process.env.GROQ_API_KEY;
|
||||
if (groqKey && groqKey.trim()) {
|
||||
return {
|
||||
id: "provider-default-env-fallback",
|
||||
name: "Groq (Env Fallback)",
|
||||
providerName: "groq",
|
||||
apiKey: groqKey.trim(),
|
||||
isActive: true,
|
||||
modelName: "llama-3.3-70b-versatile",
|
||||
type: "generative",
|
||||
maxContext: 8192,
|
||||
};
|
||||
}
|
||||
const deepseekKey = process.env.DEEPSEEK_API_KEY;
|
||||
if (deepseekKey && deepseekKey.trim()) {
|
||||
return {
|
||||
id: "provider-default-env-fallback",
|
||||
name: "DeepSeek (Env Fallback)",
|
||||
providerName: "deepseek",
|
||||
apiKey: deepseekKey.trim(),
|
||||
isActive: true,
|
||||
modelName: "deepseek-chat",
|
||||
type: "generative",
|
||||
maxContext: 64000,
|
||||
};
|
||||
}
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
return {
|
||||
|
||||
114
packages/llm/src/providers/deepseek.ts
Normal file
114
packages/llm/src/providers/deepseek.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { z } from "zod";
|
||||
import { ChatDeepSeek } from "@langchain/deepseek";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
} from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
import { ProviderManager } from "../provider-manager.js";
|
||||
|
||||
export class DeepSeekProvider implements ILLMProvider {
|
||||
static readonly providerId = "deepseek";
|
||||
static readonly displayName = "DeepSeek";
|
||||
static readonly description =
|
||||
"Official DeepSeek integration using @langchain/deepseek SDK";
|
||||
static readonly defaultModel = "deepseek-chat";
|
||||
|
||||
providerName = "DeepSeek";
|
||||
private model: ChatDeepSeek;
|
||||
private modelNameUsed: string;
|
||||
private providerInstanceName?: string;
|
||||
private maxContextUsed?: number;
|
||||
lastCalls: LLMCallRecord[] = [];
|
||||
|
||||
constructor(
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
providerInstanceName?: string,
|
||||
maxContext?: number,
|
||||
) {
|
||||
let key = apiKey;
|
||||
let model = modelName;
|
||||
this.providerInstanceName = providerInstanceName;
|
||||
this.maxContextUsed = maxContext;
|
||||
|
||||
if (!key) {
|
||||
const active = ProviderManager.getActive("generative");
|
||||
if (active && active.providerName === DeepSeekProvider.providerId) {
|
||||
key = active.apiKey;
|
||||
if (!model) {
|
||||
model = active.modelName;
|
||||
}
|
||||
if (!this.providerInstanceName) {
|
||||
this.providerInstanceName = active.name;
|
||||
}
|
||||
if (this.maxContextUsed === undefined) {
|
||||
this.maxContextUsed = active.maxContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
key = llmConfig.DEEPSEEK_API_KEY;
|
||||
if (!this.providerInstanceName && key) {
|
||||
this.providerInstanceName = "Environment Variable";
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
"DEEPSEEK_API_KEY is required to initialize DeepSeekProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.modelNameUsed = model || DeepSeekProvider.defaultModel;
|
||||
this.model = new ChatDeepSeek({
|
||||
apiKey: key,
|
||||
model: this.modelNameUsed,
|
||||
});
|
||||
}
|
||||
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, {
|
||||
includeRaw: true,
|
||||
});
|
||||
const result = (await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
])) as unknown as {
|
||||
parsed?: z.infer<T>;
|
||||
raw?: {
|
||||
usage_metadata?: {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
total_tokens?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const parsed = result?.parsed;
|
||||
const raw = result?.raw;
|
||||
|
||||
const usage = {
|
||||
inputTokens: raw?.usage_metadata?.input_tokens || 0,
|
||||
outputTokens: raw?.usage_metadata?.output_tokens || 0,
|
||||
totalTokens: raw?.usage_metadata?.total_tokens || 0,
|
||||
modelName: this.modelNameUsed,
|
||||
providerInstanceName: this.providerInstanceName || "Default",
|
||||
maxContext:
|
||||
this.maxContextUsed !== undefined ? this.maxContextUsed : 64000,
|
||||
};
|
||||
|
||||
this.lastCalls.push({
|
||||
systemPrompt: request.systemPrompt,
|
||||
userContext: request.userContext,
|
||||
usage,
|
||||
});
|
||||
|
||||
return { success: true, data: parsed, usage };
|
||||
}
|
||||
}
|
||||
112
packages/llm/src/providers/groq.ts
Normal file
112
packages/llm/src/providers/groq.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { z } from "zod";
|
||||
import { ChatGroq } from "@langchain/groq";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
} from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
import { ProviderManager } from "../provider-manager.js";
|
||||
|
||||
export class GroqProvider implements ILLMProvider {
|
||||
static readonly providerId = "groq";
|
||||
static readonly displayName = "Groq";
|
||||
static readonly description =
|
||||
"Official Groq integration using @langchain/groq SDK";
|
||||
static readonly defaultModel = "llama-3.3-70b-versatile";
|
||||
|
||||
providerName = "Groq";
|
||||
private model: ChatGroq;
|
||||
private modelNameUsed: string;
|
||||
private providerInstanceName?: string;
|
||||
private maxContextUsed?: number;
|
||||
lastCalls: LLMCallRecord[] = [];
|
||||
|
||||
constructor(
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
providerInstanceName?: string,
|
||||
maxContext?: number,
|
||||
) {
|
||||
let key = apiKey;
|
||||
let model = modelName;
|
||||
this.providerInstanceName = providerInstanceName;
|
||||
this.maxContextUsed = maxContext;
|
||||
|
||||
if (!key) {
|
||||
const active = ProviderManager.getActive("generative");
|
||||
if (active && active.providerName === GroqProvider.providerId) {
|
||||
key = active.apiKey;
|
||||
if (!model) {
|
||||
model = active.modelName;
|
||||
}
|
||||
if (!this.providerInstanceName) {
|
||||
this.providerInstanceName = active.name;
|
||||
}
|
||||
if (this.maxContextUsed === undefined) {
|
||||
this.maxContextUsed = active.maxContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
key = llmConfig.GROQ_API_KEY;
|
||||
if (!this.providerInstanceName && key) {
|
||||
this.providerInstanceName = "Environment Variable";
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("GROQ_API_KEY is required to initialize GroqProvider");
|
||||
}
|
||||
|
||||
this.modelNameUsed = model || GroqProvider.defaultModel;
|
||||
this.model = new ChatGroq({
|
||||
apiKey: key,
|
||||
model: this.modelNameUsed,
|
||||
});
|
||||
}
|
||||
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, {
|
||||
includeRaw: true,
|
||||
});
|
||||
const result = (await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
])) as unknown as {
|
||||
parsed?: z.infer<T>;
|
||||
raw?: {
|
||||
usage_metadata?: {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
total_tokens?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const parsed = result?.parsed;
|
||||
const raw = result?.raw;
|
||||
|
||||
const usage = {
|
||||
inputTokens: raw?.usage_metadata?.input_tokens || 0,
|
||||
outputTokens: raw?.usage_metadata?.output_tokens || 0,
|
||||
totalTokens: raw?.usage_metadata?.total_tokens || 0,
|
||||
modelName: this.modelNameUsed,
|
||||
providerInstanceName: this.providerInstanceName || "Default",
|
||||
maxContext:
|
||||
this.maxContextUsed !== undefined ? this.maxContextUsed : 8192,
|
||||
};
|
||||
|
||||
this.lastCalls.push({
|
||||
systemPrompt: request.systemPrompt,
|
||||
userContext: request.userContext,
|
||||
usage,
|
||||
});
|
||||
|
||||
return { success: true, data: parsed, usage };
|
||||
}
|
||||
}
|
||||
98
packages/llm/tests/deepseek.test.ts
Normal file
98
packages/llm/tests/deepseek.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { DeepSeekProvider } from "../src/providers/deepseek.js";
|
||||
import { llmConfig } from "../src/config.js";
|
||||
|
||||
// Mock the ChatDeepSeek class
|
||||
vi.mock("@langchain/deepseek", () => {
|
||||
return {
|
||||
ChatDeepSeek: class {
|
||||
config: unknown;
|
||||
constructor(config: unknown) {
|
||||
this.config = config;
|
||||
}
|
||||
withStructuredOutput = vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
invoke: vi.fn().mockImplementation(async () => {
|
||||
return {
|
||||
parsed: {
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
},
|
||||
raw: {
|
||||
usage_metadata: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("DeepSeekProvider Unit Tests (Tier 1)", () => {
|
||||
test("initializes successfully with a provided apiKey", () => {
|
||||
const provider = new DeepSeekProvider("dummy-key");
|
||||
expect(provider.providerName).toBe("DeepSeek");
|
||||
});
|
||||
|
||||
test("initializes successfully with apiKey from config", () => {
|
||||
const originalKey = llmConfig.DEEPSEEK_API_KEY;
|
||||
llmConfig.DEEPSEEK_API_KEY = "env-dummy-key";
|
||||
|
||||
try {
|
||||
const provider = new DeepSeekProvider();
|
||||
expect(provider.providerName).toBe("DeepSeek");
|
||||
} finally {
|
||||
llmConfig.DEEPSEEK_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("throws error if no API key is provided or in config", () => {
|
||||
const originalKey = llmConfig.DEEPSEEK_API_KEY;
|
||||
llmConfig.DEEPSEEK_API_KEY = undefined;
|
||||
|
||||
try {
|
||||
expect(() => new DeepSeekProvider()).toThrow(
|
||||
"DEEPSEEK_API_KEY is required to initialize DeepSeekProvider",
|
||||
);
|
||||
} finally {
|
||||
llmConfig.DEEPSEEK_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("generateStructuredResponse invokes the model with structured output, records usage and updates lastCalls", async () => {
|
||||
const provider = new DeepSeekProvider("dummy-key");
|
||||
const TestSchema = z.object({
|
||||
name: z.string(),
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
const response = await provider.generateStructuredResponse({
|
||||
systemPrompt: "system prompt",
|
||||
userContext: "user context",
|
||||
schema: TestSchema,
|
||||
});
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toEqual({
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect(response.usage).toEqual({
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
totalTokens: 15,
|
||||
modelName: "deepseek-chat",
|
||||
providerInstanceName: "Default",
|
||||
maxContext: 64000,
|
||||
});
|
||||
|
||||
expect(provider.lastCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
98
packages/llm/tests/groq.test.ts
Normal file
98
packages/llm/tests/groq.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { GroqProvider } from "../src/providers/groq.js";
|
||||
import { llmConfig } from "../src/config.js";
|
||||
|
||||
// Mock the ChatGroq class
|
||||
vi.mock("@langchain/groq", () => {
|
||||
return {
|
||||
ChatGroq: class {
|
||||
config: unknown;
|
||||
constructor(config: unknown) {
|
||||
this.config = config;
|
||||
}
|
||||
withStructuredOutput = vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
invoke: vi.fn().mockImplementation(async () => {
|
||||
return {
|
||||
parsed: {
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
},
|
||||
raw: {
|
||||
usage_metadata: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("GroqProvider Unit Tests (Tier 1)", () => {
|
||||
test("initializes successfully with a provided apiKey", () => {
|
||||
const provider = new GroqProvider("dummy-key");
|
||||
expect(provider.providerName).toBe("Groq");
|
||||
});
|
||||
|
||||
test("initializes successfully with apiKey from config", () => {
|
||||
const originalKey = llmConfig.GROQ_API_KEY;
|
||||
llmConfig.GROQ_API_KEY = "env-dummy-key";
|
||||
|
||||
try {
|
||||
const provider = new GroqProvider();
|
||||
expect(provider.providerName).toBe("Groq");
|
||||
} finally {
|
||||
llmConfig.GROQ_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("throws error if no API key is provided or in config", () => {
|
||||
const originalKey = llmConfig.GROQ_API_KEY;
|
||||
llmConfig.GROQ_API_KEY = undefined;
|
||||
|
||||
try {
|
||||
expect(() => new GroqProvider()).toThrow(
|
||||
"GROQ_API_KEY is required to initialize GroqProvider",
|
||||
);
|
||||
} finally {
|
||||
llmConfig.GROQ_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("generateStructuredResponse invokes the model with structured output, records usage and updates lastCalls", async () => {
|
||||
const provider = new GroqProvider("dummy-key");
|
||||
const TestSchema = z.object({
|
||||
name: z.string(),
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
const response = await provider.generateStructuredResponse({
|
||||
systemPrompt: "system prompt",
|
||||
userContext: "user context",
|
||||
schema: TestSchema,
|
||||
});
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toEqual({
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect(response.usage).toEqual({
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
totalTokens: 15,
|
||||
modelName: "llama-3.3-70b-versatile",
|
||||
providerInstanceName: "Default",
|
||||
maxContext: 8192,
|
||||
});
|
||||
|
||||
expect(provider.lastCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
135
packages/llm/tests/openai.test.ts
Normal file
135
packages/llm/tests/openai.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
OpenAIProvider,
|
||||
OpenAIEmbeddingProvider,
|
||||
} from "../src/providers/openai.js";
|
||||
import { llmConfig } from "../src/config.js";
|
||||
|
||||
// Mock the ChatOpenAI and OpenAIEmbeddings classes
|
||||
vi.mock("@langchain/openai", () => {
|
||||
return {
|
||||
ChatOpenAI: class {
|
||||
config: unknown;
|
||||
constructor(config: unknown) {
|
||||
this.config = config;
|
||||
}
|
||||
withStructuredOutput = vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
invoke: vi.fn().mockImplementation(async () => {
|
||||
return {
|
||||
parsed: {
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
},
|
||||
raw: {
|
||||
usage_metadata: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
OpenAIEmbeddings: class {
|
||||
config: unknown;
|
||||
constructor(config: unknown) {
|
||||
this.config = config;
|
||||
}
|
||||
embedQuery = vi.fn().mockImplementation(async (text: string) => {
|
||||
return [0.1, 0.2, 0.3];
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("OpenAIProvider Unit Tests (Tier 1)", () => {
|
||||
test("initializes successfully with a provided apiKey", () => {
|
||||
const provider = new OpenAIProvider("dummy-key");
|
||||
expect(provider.providerName).toBe("OpenAI");
|
||||
});
|
||||
|
||||
test("initializes successfully with apiKey from config", () => {
|
||||
const originalKey = llmConfig.OPENAI_API_KEY;
|
||||
llmConfig.OPENAI_API_KEY = "env-dummy-key";
|
||||
|
||||
try {
|
||||
const provider = new OpenAIProvider();
|
||||
expect(provider.providerName).toBe("OpenAI");
|
||||
} finally {
|
||||
llmConfig.OPENAI_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("throws error if no API key is provided or in config", () => {
|
||||
const originalKey = llmConfig.OPENAI_API_KEY;
|
||||
llmConfig.OPENAI_API_KEY = undefined;
|
||||
|
||||
try {
|
||||
expect(() => new OpenAIProvider()).toThrow(
|
||||
"OPENAI_API_KEY is required to initialize OpenAIProvider",
|
||||
);
|
||||
} finally {
|
||||
llmConfig.OPENAI_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("generateStructuredResponse invokes the model with structured output, records usage and updates lastCalls", async () => {
|
||||
const provider = new OpenAIProvider("dummy-key");
|
||||
const TestSchema = z.object({
|
||||
name: z.string(),
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
const response = await provider.generateStructuredResponse({
|
||||
systemPrompt: "system prompt",
|
||||
userContext: "user context",
|
||||
schema: TestSchema,
|
||||
});
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toEqual({
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
});
|
||||
|
||||
expect(response.usage).toEqual({
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
totalTokens: 15,
|
||||
modelName: "gpt-4o-mini",
|
||||
providerInstanceName: "Default",
|
||||
maxContext: 128000,
|
||||
});
|
||||
|
||||
expect(provider.lastCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAIEmbeddingProvider Unit Tests (Tier 1)", () => {
|
||||
test("initializes successfully with a provided apiKey", () => {
|
||||
const provider = new OpenAIEmbeddingProvider("dummy-key");
|
||||
expect(provider.providerName).toBe("OpenAI");
|
||||
});
|
||||
|
||||
test("initializes successfully with apiKey from config", () => {
|
||||
const originalKey = llmConfig.OPENAI_API_KEY;
|
||||
llmConfig.OPENAI_API_KEY = "env-dummy-key";
|
||||
|
||||
try {
|
||||
const provider = new OpenAIEmbeddingProvider();
|
||||
expect(provider.providerName).toBe("OpenAI");
|
||||
} finally {
|
||||
llmConfig.OPENAI_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("embed returns dummy array successfully", async () => {
|
||||
const provider = new OpenAIEmbeddingProvider("dummy-key");
|
||||
const result = await provider.embed("hello");
|
||||
expect(result).toEqual([0.1, 0.2, 0.3]);
|
||||
});
|
||||
});
|
||||
66
pnpm-lock.yaml
generated
66
pnpm-lock.yaml
generated
@@ -263,9 +263,15 @@ importers:
|
||||
"@langchain/anthropic":
|
||||
specifier: ^0.3.11
|
||||
version: 0.3.34(zod@4.4.3)
|
||||
"@langchain/deepseek":
|
||||
specifier: ^1.1.5
|
||||
version: 1.1.5(ws@8.21.0)
|
||||
"@langchain/google-genai":
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))
|
||||
"@langchain/groq":
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
"@langchain/ollama":
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.4
|
||||
@@ -2086,6 +2092,15 @@ packages:
|
||||
}
|
||||
engines: { node: ">=20" }
|
||||
|
||||
"@langchain/deepseek@1.1.5":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-5IRoEUaHAgIF8TyIncNVhhjavCqsjWTjakWsnus1yJN2X3W15Bw8Qmf+vJzCnFo7yndICsGdOGfHJoIN5xNxoQ==,
|
||||
}
|
||||
engines: { node: ">=20" }
|
||||
peerDependencies:
|
||||
"@langchain/core": ^1.0.0
|
||||
|
||||
"@langchain/google-genai@2.2.0":
|
||||
resolution:
|
||||
{
|
||||
@@ -2095,6 +2110,15 @@ packages:
|
||||
peerDependencies:
|
||||
"@langchain/core": ^1.2.0
|
||||
|
||||
"@langchain/groq@1.3.1":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-ImxfGBis4FEHpZdeT6dot6V6l09uBLYm/1BHQ6x3XEQmJFF7aKwbOeSOV5h/h5IeRx+2gaInR+LfyYoqT8satQ==,
|
||||
}
|
||||
engines: { node: ">=20" }
|
||||
peerDependencies:
|
||||
"@langchain/core": ^1.1.30
|
||||
|
||||
"@langchain/ollama@0.2.4":
|
||||
resolution:
|
||||
{
|
||||
@@ -2122,6 +2146,15 @@ packages:
|
||||
peerDependencies:
|
||||
"@langchain/core": ^1.2.1
|
||||
|
||||
"@langchain/openai@1.5.5":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-wX7dwb9z4nf5FHXlIl/X2mk08pzonvRHCt1D4+s1zXLP0duYDC95j7dulPIQJ6fmhbyYQc9Ki8mEhY/D1lB8kw==,
|
||||
}
|
||||
engines: { node: ">=20" }
|
||||
peerDependencies:
|
||||
"@langchain/core": ^1.2.2
|
||||
|
||||
"@langchain/openrouter@0.4.3":
|
||||
resolution:
|
||||
{
|
||||
@@ -6081,6 +6114,13 @@ packages:
|
||||
integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==,
|
||||
}
|
||||
|
||||
groq-sdk@1.3.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-mvgUIpAxlk/VxWIoliHx4R+Ha78Bd/g0t24OFjCXtdLbNiY1rW4h9AcznKBwFho7K/Nq732ZSjxMYkNb1xeCFg==,
|
||||
}
|
||||
hasBin: true
|
||||
|
||||
h3@1.15.11:
|
||||
resolution:
|
||||
{
|
||||
@@ -10631,11 +10671,24 @@ snapshots:
|
||||
- openai
|
||||
- ws
|
||||
|
||||
"@langchain/deepseek@1.1.5(ws@8.21.0)":
|
||||
dependencies:
|
||||
"@langchain/openai": 1.5.5(ws@8.21.0)
|
||||
transitivePeerDependencies:
|
||||
- "@aws-sdk/credential-provider-node"
|
||||
- "@smithy/hash-node"
|
||||
- "@smithy/signature-v4"
|
||||
- ws
|
||||
|
||||
"@langchain/google-genai@2.2.0(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))":
|
||||
dependencies:
|
||||
"@google/generative-ai": 0.24.1
|
||||
"@langchain/core": 1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)
|
||||
|
||||
"@langchain/groq@1.3.1":
|
||||
dependencies:
|
||||
groq-sdk: 1.3.0
|
||||
|
||||
"@langchain/ollama@0.2.4":
|
||||
dependencies:
|
||||
ollama: 0.5.18
|
||||
@@ -10662,6 +10715,17 @@ snapshots:
|
||||
- "@smithy/signature-v4"
|
||||
- ws
|
||||
|
||||
"@langchain/openai@1.5.5(ws@8.21.0)":
|
||||
dependencies:
|
||||
js-tiktoken: 1.0.21
|
||||
openai: 6.45.0(ws@8.21.0)(zod@4.4.3)
|
||||
zod: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- "@aws-sdk/credential-provider-node"
|
||||
- "@smithy/hash-node"
|
||||
- "@smithy/signature-v4"
|
||||
- ws
|
||||
|
||||
"@langchain/openrouter@0.4.3(ws@8.21.0)(zod@4.4.3)":
|
||||
dependencies:
|
||||
"@langchain/openai": 1.5.3(ws@8.21.0)
|
||||
@@ -13315,6 +13379,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
groq-sdk@1.3.0: {}
|
||||
|
||||
h3@1.15.11:
|
||||
dependencies:
|
||||
cookie-es: 1.2.3
|
||||
|
||||
Reference in New Issue
Block a user