Skip to content

Commit d0ed6f1

Browse files
committed
Add support for OpenRouter as an LLM provider.
1 parent a5a2e30 commit d0ed6f1

File tree

11 files changed

+52
-10
lines changed

11 files changed

+52
-10
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Snap a photo of any receipt or upload an invoice PDF, and TaxHacker will automat
4040
- **Auto-categorization**: Transactions are automatically sorted into relevant categories based on their content
4141
- **Item splitting**: Extract individual items from invoices and split them into separate transactions when needed
4242
- **Structured storage**: Everything gets saved in an organized database for easy filtering and retrieval
43-
- **Customizable AI providers**: Choose from OpenAI, Google Gemini, or Mistral (local LLM support coming soon)
43+
- **Customizable AI providers**: Choose from OpenAI, Google Gemini, Mistral and OpenRouter (local LLM support coming soon)
4444

4545
TaxHacker works with a wide variety of documents, including store receipts, restaurant bills, invoices, bank statements, letters, even handwritten receipts. It handles any language and any currency with ease.
4646

@@ -163,6 +163,7 @@ You can also configure LLM provider settings in the application or via environme
163163
- **OpenAI**: `OPENAI_MODEL_NAME` and `OPENAI_API_KEY`
164164
- **Google Gemini**: `GOOGLE_MODEL_NAME` and `GOOGLE_API_KEY`
165165
- **Mistral**: `MISTRAL_MODEL_NAME` and `MISTRAL_API_KEY`
166+
- **OpenRouter**: `OPENROUTER_MODEL_NAME` and `OPENROUTER_API_KEY`
166167

167168
## ⌨️ Local Development
168169

ai/providers/llmProvider.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ChatGoogleGenerativeAI } from "@langchain/google-genai"
33
import { ChatMistralAI } from "@langchain/mistralai"
44
import { BaseMessage, HumanMessage } from "@langchain/core/messages"
55

6-
export type LLMProvider = "openai" | "google" | "mistral"
6+
export type LLMProvider = "openai" | "google" | "mistral" | "openrouter"
77

88
export interface LLMConfig {
99
provider: LLMProvider
@@ -50,6 +50,15 @@ async function requestLLMUnified(config: LLMConfig, req: LLMRequest): Promise<LL
5050
model: config.model,
5151
temperature: temperature,
5252
})
53+
} else if (config.provider === "openrouter") {
54+
model = new ChatOpenAI({
55+
apiKey: config.apiKey,
56+
model: config.model,
57+
temperature: temperature,
58+
configuration: {
59+
baseURL: "https://openrouter.ai/api/v1",
60+
},
61+
})
5362
} else {
5463
return {
5564
output: {},

app/(app)/unsorted/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export default async function UnsortedPage() {
4141
{config.selfHosted.isEnabled &&
4242
!settings.openai_api_key &&
4343
!settings.google_api_key &&
44-
!settings.mistral_api_key && (
44+
!settings.mistral_api_key &&
45+
!settings.openrouter_api_key && (
4546
<Alert>
4647
<Settings className="h-4 w-4 mt-2" />
4748
<div className="flex flex-row justify-between pt-2">

app/(auth)/actions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export async function selfHostedGetStartedAction(formData: FormData) {
1616
const apiKeys = [
1717
"openai_api_key",
1818
"google_api_key",
19-
"mistral_api_key"
19+
"mistral_api_key",
20+
"openrouter_api_key"
2021
]
2122

2223
for (const key of apiKeys) {

app/(auth)/self-hosted/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default async function SelfHostedWelcomePage() {
3737
openai: config.ai.openaiApiKey ?? "",
3838
google: config.ai.googleApiKey ?? "",
3939
mistral: config.ai.mistralApiKey ?? "",
40+
openrouter: config.ai.openrouterApiKey ?? "",
4041
}
4142

4243
return (

components/settings/llm-settings-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { PROVIDERS } from "@/lib/llm-providers";
3030
function getInitialProviderOrder(settings: Record<string, string>) {
3131
let order: string[] = []
3232
if (!settings.llm_providers) {
33-
order = ['openai', 'google', 'mistral']
33+
order = ['openai', 'google', 'mistral', 'openrouter']
3434
} else {
3535
order = settings.llm_providers.split(",").map(p => p.trim())
3636
}

forms/settings.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export const settingsFormSchema = z.object({
1212
google_model_name: z.string().default("gemini-2.5-flash"),
1313
mistral_api_key: z.string().optional(),
1414
mistral_model_name: z.string().default("mistral-medium-latest"),
15-
llm_providers: z.string().default('openai,google,mistral'),
15+
openrouter_api_key: z.string().optional(),
16+
openrouter_model_name: z.string().default("openai/gpt-4o-mini"),
17+
llm_providers: z.string().default('openai,google,mistral,openrouter'),
1618
prompt_analyse_new_file: z.string().optional(),
1719
is_welcome_message_hidden: z.string().optional(),
1820
})

lib/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const envSchema = z.object({
1010
GOOGLE_MODEL_NAME: z.string().default("gemini-2.5-flash"),
1111
MISTRAL_API_KEY: z.string().optional(),
1212
MISTRAL_MODEL_NAME: z.string().default("mistral-medium-latest"),
13+
OPENROUTER_API_KEY: z.string().optional(),
14+
OPENROUTER_MODEL_NAME: z.string().default("gpt-4o-mini"),
1315
BETTER_AUTH_SECRET: z
1416
.string()
1517
.min(16, "Auth secret must be at least 16 characters")

lib/llm-providers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,19 @@ export const PROVIDERS = [
4444
},
4545
logo: "/logo/mistral.svg"
4646
},
47+
{
48+
key: "openrouter",
49+
label: "OpenRouter",
50+
apiKeyName: "openrouter_api_key",
51+
modelName: "openrouter_model_name",
52+
defaultModelName: "openai/gpt-4.1-mini",
53+
apiDoc: "https://openrouter.ai/keys",
54+
apiDocLabel: "OpenRouter Keys",
55+
placeholder: "sk-or-...",
56+
help: {
57+
url: "https://openrouter.ai/keys",
58+
label: "OpenRouter Keys"
59+
},
60+
logo: "/logo/openrouter.svg"
61+
},
4762
]

models/settings.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,37 @@ export type SettingsMap = Record<string, string>
99
* Helper to extract LLM provider settings from SettingsMap.
1010
*/
1111
export function getLLMSettings(settings: SettingsMap) {
12-
const priorities = (settings.llm_providers || "openai,google,mistral").split(",").map(p => p.trim()).filter(Boolean)
12+
const priorities = (settings.llm_providers || "openai,google,mistral,openrouter").split(",").map(p => p.trim()).filter(Boolean)
1313

1414
const providers = priorities.map((provider) => {
15+
const providerConfig = PROVIDERS.find(p => p.key === provider)
16+
1517
if (provider === "openai") {
1618
return {
1719
provider: provider as LLMProvider,
1820
apiKey: settings.openai_api_key || "",
19-
model: settings.openai_model_name || PROVIDERS[0]['defaultModelName'],
21+
model: settings.openai_model_name || (providerConfig?.defaultModelName),
2022
}
2123
}
2224
if (provider === "google") {
2325
return {
2426
provider: provider as LLMProvider,
2527
apiKey: settings.google_api_key || "",
26-
model: settings.google_model_name || PROVIDERS[1]['defaultModelName'],
28+
model: settings.google_model_name || (providerConfig?.defaultModelName),
2729
}
2830
}
2931
if (provider === "mistral") {
3032
return {
3133
provider: provider as LLMProvider,
3234
apiKey: settings.mistral_api_key || "",
33-
model: settings.mistral_model_name || PROVIDERS[2]['defaultModelName'],
35+
model: settings.mistral_model_name || (providerConfig?.defaultModelName),
36+
}
37+
}
38+
if (provider === "openrouter") {
39+
return {
40+
provider: provider as LLMProvider,
41+
apiKey: settings.openrouter_api_key || "",
42+
model: settings.openrouter_model_name || (providerConfig?.defaultModelName),
3443
}
3544
}
3645
return null

0 commit comments

Comments
 (0)