mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
feat: Added openrouter provider
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/google-genai": "^2.2.0",
|
||||
"@langchain/openrouter": "^0.4.3",
|
||||
"@types/node": "^20.19.43",
|
||||
"dotenv": "^17.4.2"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
const LLMConfigSchema = z.object({
|
||||
GOOGLE_API_KEY: z.string().optional(),
|
||||
OPENROUTER_API_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const llmConfig = LLMConfigSchema.parse(process.env);
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./llm.js";
|
||||
export * from "./config.js";
|
||||
export * from "./providers/google-genai.js";
|
||||
export * from "./providers/mock.js";
|
||||
export * from "./providers/openrouter.js";
|
||||
|
||||
31
packages/llm/src/providers/openrouter.ts
Normal file
31
packages/llm/src/providers/openrouter.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
import { ChatOpenRouter } from "@langchain/openrouter";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse } from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
|
||||
export class OpenRouterProvider implements ILLMProvider {
|
||||
providerName = "OpenRouter";
|
||||
private model: ChatOpenRouter;
|
||||
|
||||
constructor(apiKey?: string, modelName: string = "google/gemini-2.5-flash") {
|
||||
const key = apiKey || llmConfig.OPENROUTER_API_KEY;
|
||||
if (!key) {
|
||||
throw new Error("OPENROUTER_API_KEY is required to initialize OpenRouterProvider");
|
||||
}
|
||||
this.model = new ChatOpenRouter({
|
||||
apiKey: key,
|
||||
model: modelName,
|
||||
});
|
||||
}
|
||||
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema);
|
||||
const result = await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
]);
|
||||
return { success: true, data: result as z.infer<T> };
|
||||
}
|
||||
}
|
||||
81
packages/llm/tests/openrouter.test.ts
Normal file
81
packages/llm/tests/openrouter.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { OpenRouterProvider } from "../src/providers/openrouter.js";
|
||||
import { llmConfig } from "../src/config.js";
|
||||
|
||||
// Mock the ChatOpenRouter class
|
||||
vi.mock("@langchain/openrouter", () => {
|
||||
return {
|
||||
ChatOpenRouter: class {
|
||||
config: unknown;
|
||||
constructor(config: unknown) {
|
||||
this.config = config;
|
||||
}
|
||||
withStructuredOutput = vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
invoke: vi.fn().mockImplementation(async () => {
|
||||
// Return a mock output that matches a sample schema
|
||||
return {
|
||||
name: "mocked response",
|
||||
success: true,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("OpenRouterProvider Unit Tests (Tier 1)", () => {
|
||||
test("initializes successfully with a provided apiKey", () => {
|
||||
const provider = new OpenRouterProvider("dummy-key");
|
||||
expect(provider.providerName).toBe("OpenRouter");
|
||||
});
|
||||
|
||||
test("initializes successfully with apiKey from config", () => {
|
||||
// Save current config
|
||||
const originalKey = llmConfig.OPENROUTER_API_KEY;
|
||||
llmConfig.OPENROUTER_API_KEY = "env-dummy-key";
|
||||
|
||||
try {
|
||||
const provider = new OpenRouterProvider();
|
||||
expect(provider.providerName).toBe("OpenRouter");
|
||||
} finally {
|
||||
llmConfig.OPENROUTER_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("throws error if no API key is provided or in config", () => {
|
||||
// Save current config
|
||||
const originalKey = llmConfig.OPENROUTER_API_KEY;
|
||||
llmConfig.OPENROUTER_API_KEY = undefined;
|
||||
|
||||
try {
|
||||
expect(() => new OpenRouterProvider()).toThrow(
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider"
|
||||
);
|
||||
} finally {
|
||||
llmConfig.OPENROUTER_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
|
||||
test("generateStructuredResponse invokes the model with structured output", async () => {
|
||||
const provider = new OpenRouterProvider("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,
|
||||
});
|
||||
});
|
||||
});
|
||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -209,7 +209,10 @@ importers:
|
||||
dependencies:
|
||||
'@langchain/google-genai':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(@langchain/core@1.2.1)
|
||||
version: 2.2.0(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))
|
||||
'@langchain/openrouter':
|
||||
specifier: ^0.4.3
|
||||
version: 0.4.3(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))(zod@4.4.3)
|
||||
'@types/node':
|
||||
specifier: ^20.19.43
|
||||
version: 20.19.43
|
||||
@@ -376,7 +379,7 @@ importers:
|
||||
dependencies:
|
||||
'@langchain/google-genai':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(@langchain/core@1.2.1)
|
||||
version: 2.2.0(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))
|
||||
'@types/node':
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
@@ -1259,6 +1262,18 @@ packages:
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.2.0
|
||||
|
||||
'@langchain/openai@1.5.3':
|
||||
resolution: {integrity: sha512-OStS2AUvy9oe/hEf/3ndBOFztUDOfuJYLNXh89m3iiJAI2Cp5Dp0n/pvpO27MO0b+VgENd+xSHVyQZ7fe+ulxg==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.2.1
|
||||
|
||||
'@langchain/openrouter@0.4.3':
|
||||
resolution: {integrity: sha512-LUul0i3VAlQBkhOg0EH1N20DUOxHGAZ1gqQtX1IRO/jYbDL3HZ7uHUupjMnerDaULiyAJZT3DxDw6Cr1QMT4ng==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.0.0
|
||||
|
||||
'@mdx-js/mdx@3.1.1':
|
||||
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
|
||||
|
||||
@@ -2584,6 +2599,10 @@ packages:
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
eventsource-parser@3.1.0:
|
||||
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3561,6 +3580,26 @@ packages:
|
||||
oniguruma-to-es@4.3.6:
|
||||
resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
|
||||
|
||||
openai@6.45.0:
|
||||
resolution: {integrity: sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==}
|
||||
peerDependencies:
|
||||
'@aws-sdk/credential-provider-node': '>=3.972.0 <4'
|
||||
'@smithy/hash-node': '>=4.3.0 <5'
|
||||
'@smithy/signature-v4': '>=5.4.0 <6'
|
||||
ws: ^8.18.0
|
||||
zod: ^3.25 || ^4.0
|
||||
peerDependenciesMeta:
|
||||
'@aws-sdk/credential-provider-node':
|
||||
optional: true
|
||||
'@smithy/hash-node':
|
||||
optional: true
|
||||
'@smithy/signature-v4':
|
||||
optional: true
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5205,12 +5244,12 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@langchain/core@1.2.1':
|
||||
'@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3))':
|
||||
dependencies:
|
||||
'@cfworker/json-schema': 4.1.1
|
||||
'@standard-schema/spec': 1.1.0
|
||||
js-tiktoken: 1.0.21
|
||||
langsmith: 0.7.15
|
||||
langsmith: 0.7.15(openai@6.45.0(zod@4.4.3))
|
||||
mustache: 4.2.0
|
||||
p-queue: 6.6.2
|
||||
zod: 4.4.3
|
||||
@@ -5221,10 +5260,35 @@ snapshots:
|
||||
- openai
|
||||
- ws
|
||||
|
||||
'@langchain/google-genai@2.2.0(@langchain/core@1.2.1)':
|
||||
'@langchain/google-genai@2.2.0(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))':
|
||||
dependencies:
|
||||
'@google/generative-ai': 0.24.1
|
||||
'@langchain/core': 1.2.1
|
||||
'@langchain/core': 1.2.1(openai@6.45.0(zod@4.4.3))
|
||||
|
||||
'@langchain/openai@1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))':
|
||||
dependencies:
|
||||
'@langchain/core': 1.2.1(openai@6.45.0(zod@4.4.3))
|
||||
js-tiktoken: 1.0.21
|
||||
openai: 6.45.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(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@langchain/core': 1.2.1(openai@6.45.0(zod@4.4.3))
|
||||
'@langchain/openai': 1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@4.4.3)))
|
||||
eventsource-parser: 3.1.0
|
||||
openai: 6.45.0(zod@4.4.3)
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-provider-node'
|
||||
- '@smithy/hash-node'
|
||||
- '@smithy/signature-v4'
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mdx-js/mdx@3.1.1':
|
||||
dependencies:
|
||||
@@ -6854,6 +6918,8 @@ snapshots:
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
eventsource-parser@3.1.0: {}
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
expect-type@1.4.0: {}
|
||||
@@ -7474,9 +7540,11 @@ snapshots:
|
||||
|
||||
klona@2.0.6: {}
|
||||
|
||||
langsmith@0.7.15:
|
||||
langsmith@0.7.15(openai@6.45.0(zod@4.4.3)):
|
||||
dependencies:
|
||||
p-queue: 6.6.2
|
||||
optionalDependencies:
|
||||
openai: 6.45.0(zod@4.4.3)
|
||||
|
||||
language-subtag-registry@0.3.23: {}
|
||||
|
||||
@@ -8185,6 +8253,10 @@ snapshots:
|
||||
regex: 6.1.0
|
||||
regex-recursion: 6.0.2
|
||||
|
||||
openai@6.45.0(zod@4.4.3):
|
||||
optionalDependencies:
|
||||
zod: 4.4.3
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
|
||||
Reference in New Issue
Block a user