From 29dce8c46d2dfdc9c3b603c1c02fb8fd3eb23381 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 10:55:26 +0000 Subject: [PATCH 1/2] Make OpenAI models configurable and add OPENAI_BASE_URL Replace the deprecated gpt-3.5-turbo category-extraction model and the hardcoded receipt-scanning model with current-gen gpt-5.4-nano defaults, each overridable via OPENAI_MODEL_CATEGORY_EXTRACT and OPENAI_MODEL_RECEIPT_EXTRACT. Add optional OPENAI_BASE_URL to support self-hosted / OpenAI-compatible endpoints. Closes #517. https://claude.ai/code/session_015jgzJtUpheDpE53Rq5RPxY --- .env.example | 10 +++++++++- README.md | 10 ++++++++-- .../expenses/create-from-receipt-button-actions.ts | 7 +++++-- src/components/expense-form-actions.tsx | 7 +++++-- src/lib/env.ts | 9 +++++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 6002a9b99..6cdc2b17d 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,12 @@ POSTGRES_PRISMA_URL=postgresql://postgres:1234@localhost POSTGRES_URL_NON_POOLING=postgresql://postgres:1234@localhost -NEXT_PUBLIC_DEFAULT_CURRENCY_CODE="" \ No newline at end of file +NEXT_PUBLIC_DEFAULT_CURRENCY_CODE="" + +# OpenAI (category extraction / receipt scanning). All optional. +# OPENAI_API_KEY= +# Point at a self-hosted / OpenAI-compatible endpoint (defaults to OpenAI): +# OPENAI_BASE_URL= +# Override the models (both default to gpt-5.4-nano): +# OPENAI_MODEL_CATEGORY_EXTRACT=gpt-5.4-nano +# OPENAI_MODEL_RECEIPT_EXTRACT=gpt-5.4-nano \ No newline at end of file diff --git a/README.md b/README.md index 91ee5d155..0179d3ebe 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,12 @@ S3_UPLOAD_ENDPOINT=http://localhost:9000 ### Create expense from receipt -You can offer users to create expense by uploading a receipt. This feature relies on [OpenAI GPT-4 with Vision](https://platform.openai.com/docs/guides/vision) and a public S3 storage endpoint. +You can offer users to create expense by uploading a receipt. This feature relies on a vision-capable [OpenAI](https://platform.openai.com/docs/guides/vision) model and a public S3 storage endpoint. To enable the feature: - You must enable expense documents feature as well (see section above). That might change in the future, but for now we need to store images to make receipt scanning work. -- Subscribe to OpenAI API and get access to GPT 4 with Vision (you might need to buy credits in advance). +- Subscribe to OpenAI API and get access to a vision-capable model (you might need to buy credits in advance). - Update your environment variables with appropriate values: ```.env @@ -113,6 +113,8 @@ NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT=true OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` +The model defaults to `gpt-5.4-nano`. You can override it with the optional `OPENAI_MODEL_RECEIPT_EXTRACT` variable (e.g. `gpt-5.4-mini` for higher OCR accuracy on poor-quality photos). + ### Deduce category from title You can offer users to automatically deduce the expense category from the title. Since this feature relies on a OpenAI subscription, follow the signup instructions above and configure the following environment variables: @@ -122,6 +124,10 @@ NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT=true OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` +The model defaults to `gpt-5.4-nano`. You can override it with the optional `OPENAI_MODEL_CATEGORY_EXTRACT` variable. + +To use a self-hosted or OpenAI-compatible provider for either feature, set the optional `OPENAI_BASE_URL` variable (when unset, the official OpenAI API is used). + ## License MIT, see [LICENSE](./LICENSE). diff --git a/src/app/groups/[groupId]/expenses/create-from-receipt-button-actions.ts b/src/app/groups/[groupId]/expenses/create-from-receipt-button-actions.ts index b04dbab7c..e705ebb39 100644 --- a/src/app/groups/[groupId]/expenses/create-from-receipt-button-actions.ts +++ b/src/app/groups/[groupId]/expenses/create-from-receipt-button-actions.ts @@ -5,14 +5,17 @@ import { formatCategoryForAIPrompt } from '@/lib/utils' import OpenAI from 'openai' import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/index.mjs' -const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY }) +const openai = new OpenAI({ + apiKey: env.OPENAI_API_KEY, + baseURL: env.OPENAI_BASE_URL, +}) export async function extractExpenseInformationFromImage(imageUrl: string) { 'use server' const categories = await getCategories() const body: ChatCompletionCreateParamsNonStreaming = { - model: 'gpt-5-nano', + model: env.OPENAI_MODEL_RECEIPT_EXTRACT, messages: [ { role: 'user', diff --git a/src/components/expense-form-actions.tsx b/src/components/expense-form-actions.tsx index ef0d3b51f..917a2e032 100644 --- a/src/components/expense-form-actions.tsx +++ b/src/components/expense-form-actions.tsx @@ -5,7 +5,10 @@ import { formatCategoryForAIPrompt } from '@/lib/utils' import OpenAI from 'openai' import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/index.mjs' -const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY }) +const openai = new OpenAI({ + apiKey: env.OPENAI_API_KEY, + baseURL: env.OPENAI_BASE_URL, +}) /** Limit of characters to be evaluated. May help avoiding abuse when using AI. */ const limit = 40 // ~10 tokens @@ -19,7 +22,7 @@ export async function extractCategoryFromTitle(description: string) { const categories = await getCategories() const body: ChatCompletionCreateParamsNonStreaming = { - model: 'gpt-3.5-turbo', + model: env.OPENAI_MODEL_CATEGORY_EXTRACT, temperature: 0.1, // try to be highly deterministic so that each distinct title may lead to the same category every time max_tokens: 1, // category ids are unlikely to go beyond ~4 digits so limit possible abuse messages: [ diff --git a/src/lib/env.ts b/src/lib/env.ts index dad50259b..303ce743d 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -36,6 +36,15 @@ const envSchema = z z.boolean().default(false), ), OPENAI_API_KEY: z.string().optional(), + OPENAI_BASE_URL: z.string().url().optional(), + OPENAI_MODEL_CATEGORY_EXTRACT: z + .string() + .optional() + .default('gpt-5.4-nano'), + OPENAI_MODEL_RECEIPT_EXTRACT: z + .string() + .optional() + .default('gpt-5.4-nano'), }) .superRefine((env, ctx) => { if ( From e1162098044ad15bf089f4feb055da08b4ffac63 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 11:04:18 +0000 Subject: [PATCH 2/2] Apply prettier formatting to env schema https://claude.ai/code/session_015jgzJtUpheDpE53Rq5RPxY --- src/lib/env.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/env.ts b/src/lib/env.ts index 303ce743d..ff1373d25 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -41,10 +41,7 @@ const envSchema = z .string() .optional() .default('gpt-5.4-nano'), - OPENAI_MODEL_RECEIPT_EXTRACT: z - .string() - .optional() - .default('gpt-5.4-nano'), + OPENAI_MODEL_RECEIPT_EXTRACT: z.string().optional().default('gpt-5.4-nano'), }) .superRefine((env, ctx) => { if (