feat: Added openrouter provider

This commit is contained in:
2026-07-09 11:13:02 +05:30
parent 907c3b8ed7
commit 4339a8b4b5
6 changed files with 194 additions and 7 deletions

View File

@@ -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"
}

View File

@@ -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);

View File

@@ -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";

View 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> };
}
}

View 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
View File

@@ -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