Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ GOOGLE_API_KEY=""
MISTRAL_MODEL_NAME="mistral-medium-latest"
MISTRAL_API_KEY=""

OPENROUTER_MODEL_NAME="openai/gpt-4.1-mini"
OPENROUTER_API_KEY=""

# Auth Config
BETTER_AUTH_SECRET="random-secret-key" # please use any long random string here

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

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.

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

## ⌨️ Local Development

Expand Down
11 changes: 10 additions & 1 deletion ai/providers/llmProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ChatGoogleGenerativeAI } from "@langchain/google-genai"
import { ChatMistralAI } from "@langchain/mistralai"
import { BaseMessage, HumanMessage } from "@langchain/core/messages"

export type LLMProvider = "openai" | "google" | "mistral"
export type LLMProvider = "openai" | "google" | "mistral" | "openrouter"

export interface LLMConfig {
provider: LLMProvider
Expand Down Expand Up @@ -50,6 +50,15 @@ async function requestLLMUnified(config: LLMConfig, req: LLMRequest): Promise<LL
model: config.model,
temperature: temperature,
})
} else if (config.provider === "openrouter") {
model = new ChatOpenAI({
apiKey: config.apiKey,
model: config.model,
temperature: temperature,
configuration: {
baseURL: "https://openrouter.ai/api/v1",
},
})
} else {
return {
output: {},
Expand Down
3 changes: 2 additions & 1 deletion app/(app)/unsorted/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default async function UnsortedPage() {
{config.selfHosted.isEnabled &&
!settings.openai_api_key &&
!settings.google_api_key &&
!settings.mistral_api_key && (
!settings.mistral_api_key &&
!settings.openrouter_api_key && (
<Alert>
<Settings className="h-4 w-4 mt-2" />
<div className="flex flex-row justify-between pt-2">
Expand Down
3 changes: 2 additions & 1 deletion app/(auth)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export async function selfHostedGetStartedAction(formData: FormData) {
const apiKeys = [
"openai_api_key",
"google_api_key",
"mistral_api_key"
"mistral_api_key",
"openrouter_api_key"
]

for (const key of apiKeys) {
Expand Down
1 change: 1 addition & 0 deletions app/(auth)/self-hosted/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default async function SelfHostedWelcomePage() {
openai: config.ai.openaiApiKey ?? "",
google: config.ai.googleApiKey ?? "",
mistral: config.ai.mistralApiKey ?? "",
openrouter: config.ai.openrouterApiKey ?? "",
}

return (
Expand Down
2 changes: 1 addition & 1 deletion components/settings/llm-settings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { PROVIDERS } from "@/lib/llm-providers";
function getInitialProviderOrder(settings: Record<string, string>) {
let order: string[] = []
if (!settings.llm_providers) {
order = ['openai', 'google', 'mistral']
order = ['openai', 'google', 'mistral', 'openrouter']
} else {
order = settings.llm_providers.split(",").map(p => p.trim())
}
Expand Down
4 changes: 3 additions & 1 deletion forms/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const settingsFormSchema = z.object({
google_model_name: z.string().default("gemini-2.5-flash"),
mistral_api_key: z.string().optional(),
mistral_model_name: z.string().default("mistral-medium-latest"),
llm_providers: z.string().default('openai,google,mistral'),
openrouter_api_key: z.string().optional(),
openrouter_model_name: z.string().default("openai/gpt-4o-mini"),
llm_providers: z.string().default('openai,google,mistral,openrouter'),
prompt_analyse_new_file: z.string().optional(),
is_welcome_message_hidden: z.string().optional(),
})
Expand Down
4 changes: 4 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const envSchema = z.object({
GOOGLE_MODEL_NAME: z.string().default("gemini-2.5-flash"),
MISTRAL_API_KEY: z.string().optional(),
MISTRAL_MODEL_NAME: z.string().default("mistral-medium-latest"),
OPENROUTER_API_KEY: z.string().optional(),
OPENROUTER_MODEL_NAME: z.string().default("gpt-4o-mini"),
BETTER_AUTH_SECRET: z
.string()
.min(16, "Auth secret must be at least 16 characters")
Expand Down Expand Up @@ -59,6 +61,8 @@ const config = {
googleModelName: env.GOOGLE_MODEL_NAME,
mistralApiKey: env.MISTRAL_API_KEY,
mistralModelName: env.MISTRAL_MODEL_NAME,
openrouterApiKey: env.OPENROUTER_API_KEY,
openrouterModelName: env.OPENROUTER_MODEL_NAME,
},
auth: {
secret: env.BETTER_AUTH_SECRET,
Expand Down
15 changes: 15 additions & 0 deletions lib/llm-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,19 @@ export const PROVIDERS = [
},
logo: "/logo/mistral.svg"
},
{
key: "openrouter",
label: "OpenRouter",
apiKeyName: "openrouter_api_key",
modelName: "openrouter_model_name",
defaultModelName: "openai/gpt-4.1-mini",
apiDoc: "https://openrouter.ai/keys",
apiDocLabel: "OpenRouter Keys",
placeholder: "sk-or-...",
help: {
url: "https://openrouter.ai/keys",
label: "OpenRouter Keys"
},
logo: "/logo/openrouter.svg"
},
]
17 changes: 13 additions & 4 deletions models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,37 @@ export type SettingsMap = Record<string, string>
* Helper to extract LLM provider settings from SettingsMap.
*/
export function getLLMSettings(settings: SettingsMap) {
const priorities = (settings.llm_providers || "openai,google,mistral").split(",").map(p => p.trim()).filter(Boolean)
const priorities = (settings.llm_providers || "openai,google,mistral,openrouter").split(",").map(p => p.trim()).filter(Boolean)

const providers = priorities.map((provider) => {
const providerConfig = PROVIDERS.find(p => p.key === provider)

if (provider === "openai") {
return {
provider: provider as LLMProvider,
apiKey: settings.openai_api_key || "",
model: settings.openai_model_name || PROVIDERS[0]['defaultModelName'],
model: settings.openai_model_name || providerConfig?.defaultModelName || "gpt-4o-mini",
}
}
if (provider === "google") {
return {
provider: provider as LLMProvider,
apiKey: settings.google_api_key || "",
model: settings.google_model_name || PROVIDERS[1]['defaultModelName'],
model: settings.google_model_name || providerConfig?.defaultModelName || "gemini-2.5-flash",
}
}
if (provider === "mistral") {
return {
provider: provider as LLMProvider,
apiKey: settings.mistral_api_key || "",
model: settings.mistral_model_name || PROVIDERS[2]['defaultModelName'],
model: settings.mistral_model_name || providerConfig?.defaultModelName || "mistral-medium-latest",
}
}
if (provider === "openrouter") {
return {
provider: provider as LLMProvider,
apiKey: settings.openrouter_api_key || "",
model: settings.openrouter_model_name || providerConfig?.defaultModelName || "openai/gpt-4.1-mini",
}
}
return null
Expand Down
1 change: 1 addition & 0 deletions public/logo/openrouter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.