From 5a99273c9d4a4cd6b4f8b642bd7fa8424cfc6441 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Thu, 14 May 2026 07:48:51 +0200 Subject: [PATCH] feat: add OpenRouter API client with free model support --- app/lib/__tests__/openrouter.test.ts | 26 ++++++++++++ app/lib/openrouter.ts | 60 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 app/lib/__tests__/openrouter.test.ts create mode 100644 app/lib/openrouter.ts diff --git a/app/lib/__tests__/openrouter.test.ts b/app/lib/__tests__/openrouter.test.ts new file mode 100644 index 0000000..75e10b0 --- /dev/null +++ b/app/lib/__tests__/openrouter.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import { OpenRouterClient } from "../openrouter"; + +describe("OpenRouterClient", () => { + it("should create instance with API key", () => { + const client = new OpenRouterClient("test-api-key"); + expect(client).toBeInstanceOf(OpenRouterClient); + }); + + it("should have default free models list", () => { + const client = new OpenRouterClient("test-api-key"); + const models = client.getFreeModels(); + expect(models).toContain("google/gemini-2.0-flash-exp:free"); + }); + + it("should have available model providers", () => { + const client = new OpenRouterClient("test-api-key"); + const providers = client.getProviders(); + expect(providers).toContain("openai"); + expect(providers).toContain("google"); + expect(providers).toContain("anthropic"); + expect(providers).toContain("deepseek"); + expect(providers).toContain("meta"); + expect(providers).toContain("xai"); + }); +}); \ No newline at end of file diff --git a/app/lib/openrouter.ts b/app/lib/openrouter.ts new file mode 100644 index 0000000..c13f974 --- /dev/null +++ b/app/lib/openrouter.ts @@ -0,0 +1,60 @@ +type Message = { + role: "system" | "user" | "assistant"; + content: string; +}; + +type OpenRouterConfig = { + baseURL?: string; + defaultModel?: string; +}; + +export class OpenRouterClient { + private apiKey: string; + private baseURL: string; + private defaultModel: string; + private freeModels = [ + "google/gemini-2.0-flash-exp:free", + "deepseek/deepseek-chat:free", + "meta/llama-3.3-70b-instruct:free", + ]; + private providers = ["openai", "google", "anthropic", "deepseek", "meta", "xai"]; + + constructor(apiKey: string, config?: OpenRouterConfig) { + this.apiKey = apiKey; + this.baseURL = config?.baseURL ?? "https://openrouter.ai/api/v1"; + this.defaultModel = config?.defaultModel ?? "google/gemini-2.0-flash-exp:free"; + } + + getFreeModels(): string[] { + return [...this.freeModels]; + } + + getProviders(): string[] { + return [...this.providers]; + } + + async createChatCompletion( + messages: Message[], + model?: string + ): Promise { + const response = await fetch(`${this.baseURL}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + "HTTP-Referer": "https://aitrader.local", + "X-Title": "AITrader", + }, + body: JSON.stringify({ + model: model ?? this.defaultModel, + messages, + }), + }); + + if (!response.ok) { + throw new Error(`OpenRouter API error: ${response.status}`); + } + + return response.json(); + } +} \ No newline at end of file