feat: more llm provider options (google, mistral) (#28)

* feat: add google provider

* fix: default for google model

* feat: multiple providers

* fix: defaults from env for login form

* fix: add mistral to env files

* chore: delete unused code

* chore: revert database url to original

* fix: render default value for api key from env on server

* fix: type errors during compilation

---------

Co-authored-by: Vasily Zubarev <me@vas3k.ru>
This commit is contained in:
Dmitrii Anfimov
2025-07-22 21:49:54 +02:00
committed by GitHub
parent 9903325f17
commit dee915ffd6
21 changed files with 1185 additions and 150 deletions

View File

@@ -9,6 +9,12 @@ DATABASE_URL="postgresql://user@localhost:5432/taxhacker"
OPENAI_MODEL_NAME="gpt-4o-mini"
OPENAI_API_KEY="" # "sk-..."
GOOGLE_MODEL_NAME="gemini-2.5-flash"
GOOGLE_API_KEY=""
MISTRAL_MODEL_NAME="mistral-medium-latest"
MISTRAL_API_KEY=""
# Auth Config
BETTER_AUTH_SECRET="random-secret-key" # please use any long random string here

View File

@@ -152,8 +152,12 @@ Configure TaxHacker to suit your needs with these environment variables:
| `SELF_HOSTED_MODE` | No | Set it to "true" if you're self-hosting the app: it enables auto-login, custom API keys, and more | `true` |
| `DISABLE_SIGNUP` | No | Disable new user registration on your instance | `false` |
| `BETTER_AUTH_SECRET` | Yes | Secret key for authentication (min 16 characters) | `random-secret-key` |
| `OPENAI_MODEL_NAME` | No | OpenAI model to use for AI features | `gpt-4o-mini` |
| `OPENAI_API_KEY` | No | OpenAI API key for AI features. In self-hosted mode you can set it up in settings too. | `sk-...` |
You can also specify LLM provider options in settings or with environment variables:
- For OpenAI: `OPENAI_MODEL_NAME` and `OPENAI_API_KEY`
- For Google: `GOOGLE_MODEL_NAME` and `GOOGLE_API_KEY`
- For Mistral: `MISTRAL_MODEL_NAME` and `MISTRAL_API_KEY`
## ⌨️ Local Development

View File

@@ -1,10 +1,10 @@
"use server"
import { ActionState } from "@/lib/actions"
import config from "@/lib/config"
import OpenAI from "openai"
import { AnalyzeAttachment } from "./attachments"
import { updateFile } from "@/models/files"
import { getSettings, getLLMSettings } from "@/models/settings"
import { requestLLM } from "./providers/llmProvider"
export type AnalysisResult = {
output: Record<string, string>
@@ -15,52 +15,39 @@ export async function analyzeTransaction(
prompt: string,
schema: Record<string, unknown>,
attachments: AnalyzeAttachment[],
apiKey: string,
fileId: string,
userId: string
): Promise<ActionState<AnalysisResult>> {
const openai = new OpenAI({
apiKey,
})
console.log("RUNNING AI ANALYSIS")
console.log("PROMPT:", prompt)
console.log("SCHEMA:", schema)
const settings = await getSettings(userId)
const llmSettings = getLLMSettings(settings)
try {
const response = await openai.responses.create({
model: config.ai.modelName,
input: [
{
role: "user",
content: prompt,
},
{
role: "user",
content: attachments.map((attachment) => ({
type: "input_image",
detail: "auto",
image_url: `data:${attachment.contentType};base64,${attachment.base64}`,
})),
},
],
text: {
format: {
type: "json_schema",
name: "transaction",
schema: schema,
strict: true,
},
},
const response = await requestLLM(llmSettings, {
prompt,
schema,
attachments,
})
console.log("ChatGPT response:", response.output_text)
console.log("ChatGPT tokens used:", response.usage)
if (response.error) {
throw new Error(response.error)
}
const result = JSON.parse(response.output_text)
const result = response.output
const tokensUsed = response.tokensUsed || 0
console.log("LLM response:", result)
console.log("LLM tokens used:", tokensUsed)
await updateFile(fileId, userId, { cachedParseResult: result })
return { success: true, data: { output: result, tokensUsed: response.usage?.total_tokens || 0 } }
return {
success: true,
data: {
output: result,
tokensUsed: tokensUsed
},
}
} catch (error) {
console.error("AI Analysis error:", error)
return {

115
ai/providers/llmProvider.ts Normal file
View File

@@ -0,0 +1,115 @@
import { ChatOpenAI } from "@langchain/openai"
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 interface LLMConfig {
provider: LLMProvider
apiKey: string
model: string
}
export interface LLMSettings {
providers: LLMConfig[]
}
export interface LLMRequest {
prompt: string
schema?: Record<string, unknown>
attachments?: any[]
}
export interface LLMResponse {
output: Record<string, string>
tokensUsed?: number
provider: LLMProvider
error?: string
}
async function requestLLMUnified(config: LLMConfig, req: LLMRequest): Promise<LLMResponse> {
try {
const temperature = 0;
let model: any;
if (config.provider === "openai") {
model = new ChatOpenAI({
apiKey: config.apiKey,
model: config.model,
temperature: temperature,
});
} else if (config.provider === "google") {
model = new ChatGoogleGenerativeAI({
apiKey: config.apiKey,
model: config.model,
temperature: temperature,
});
} else if (config.provider === "mistral") {
model = new ChatMistralAI({
apiKey: config.apiKey,
model: config.model,
temperature: temperature,
});
} else {
return {
output: {},
provider: config.provider,
error: "Unknown provider",
};
}
const structuredModel = model.withStructuredOutput(req.schema, { 'name': 'transaction'});
let message_content: any = [{ type: "text", text: req.prompt }];
if (req.attachments && req.attachments.length > 0) {
const images = req.attachments.map(att => ({
type: "image_url",
image_url: {
url: `data:${att.contentType};base64,${att.base64}`
},
}));
message_content.push(...images);
}
const messages: BaseMessage[] = [
new HumanMessage({ content: message_content })
];
const response = await structuredModel.invoke(messages);
return {
output: response,
provider: config.provider,
};
} catch (error: any) {
return {
output: {},
provider: config.provider,
error: error instanceof Error ? error.message : `${config.provider} request failed`,
};
}
}
export async function requestLLM(settings: LLMSettings, req: LLMRequest): Promise<LLMResponse> {
for (const config of settings.providers) {
if (!config.apiKey || !config.model) {
console.info('Skipping provider:', config.provider);
continue;
}
console.info('Use provider:', config.provider);
const response = await requestLLMUnified(config, req);
if (!response.error) {
return response;
}
else {
console.error(response.error)
}
}
return {
output: {},
provider: settings.providers[0]?.provider || "openai",
error: "All LLM providers failed or are not configured",
};
}

View File

@@ -31,20 +31,15 @@ export async function analyzeFileAction(
const user = await getCurrentUser()
if (!file || file.userId !== user.id) {
return { success: false, error: "File not found or does not belong to the user" }
}
const apiKey = settings.openai_api_key || config.ai.openaiApiKey || ""
if (!apiKey) {
return { success: false, error: "OpenAI API key is not set" }
}
if (isAiBalanceExhausted(user)) {
return {
success: false,
error: "You used all of your pre-paid AI scans, please upgrade your account or buy new subscription plan",
return { success: false, error: "File not found or does not belong to the user" }
}
if (isAiBalanceExhausted(user)) {
return {
success: false,
error: "You used all of your pre-paid AI scans, please upgrade your account or buy new subscription plan",
}
}
}
if (isSubscriptionExpired(user)) {
return {
@@ -70,7 +65,7 @@ export async function analyzeFileAction(
const schema = fieldsToJsonSchema(fields)
const results = await analyzeTransaction(prompt, schema, attachments, apiKey, file.id, user.id)
const results = await analyzeTransaction(prompt, schema, attachments, file.id, user.id)
console.log("Analysis results:", results)

View File

@@ -38,14 +38,14 @@ export default async function UnsortedPage() {
{files.length > 1 && <AnalyzeAllButton />}
</header>
{config.selfHosted.isEnabled && !settings.openai_api_key && (
{config.selfHosted.isEnabled && !settings.openai_api_key && !settings.google_api_key && !settings.mistral_api_key && (
<Alert>
<Settings className="h-4 w-4 mt-2" />
<div className="flex flex-row justify-between pt-2">
<div className="flex flex-col">
<AlertTitle>ChatGPT API Key is required for analyzing files</AlertTitle>
<AlertTitle>LLM provider API Key is required for analyzing files</AlertTitle>
<AlertDescription>
Please set your OpenAI API key in the settings to use the analyze form.
Please set your LLM provider API key in the settings to use the analyze form.
</AlertDescription>
</div>
<Link href="/settings/llm">

View File

@@ -13,11 +13,20 @@ export async function selfHostedGetStartedAction(formData: FormData) {
await createUserDefaults(user.id)
}
const openaiApiKey = formData.get("openai_api_key")
if (openaiApiKey) {
await updateSettings(user.id, "openai_api_key", openaiApiKey as string)
const apiKeys = [
"openai_api_key",
"google_api_key",
"mistral_api_key"
]
for (const key of apiKeys) {
const value = formData.get(key)
if (value) {
await updateSettings(user.id, key, value as string)
}
}
const defaultCurrency = formData.get("default_currency")
if (defaultCurrency) {
await updateSettings(user.id, "default_currency", defaultCurrency as string)

View File

@@ -1,15 +1,12 @@
import { FormSelectCurrency } from "@/components/forms/select-currency"
import { FormInput } from "@/components/forms/simple"
import { Button } from "@/components/ui/button"
import { Card, CardDescription, CardTitle } from "@/components/ui/card"
import { ColoredText } from "@/components/ui/colored-text"
import config from "@/lib/config"
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
import { getSelfHostedUser } from "@/models/users"
import { ShieldAlert } from "lucide-react"
import Image from "next/image"
import { redirect } from "next/navigation"
import { selfHostedGetStartedAction } from "../actions"
import { PROVIDERS } from "@/lib/llm-providers"
import SelfHostedSetupFormClient from "./setup-form-client"
export default async function SelfHostedWelcomePage() {
if (!config.selfHosted.isEnabled) {
@@ -35,6 +32,14 @@ export default async function SelfHostedWelcomePage() {
redirect(config.selfHosted.redirectUrl)
}
// Собираем дефолтные ключи для всех провайдеров
const defaultProvider = PROVIDERS[0].key
const defaultApiKeys: Record<string, string> = {
openai: config.ai.openaiApiKey ?? "",
google: config.ai.googleApiKey ?? "",
mistral: config.ai.mistralApiKey ?? "",
}
return (
<Card className="w-full max-w-xl mx-auto p-8 flex flex-col items-center justify-center gap-4">
<Image src="/logo/512.png" alt="Logo" width={144} height={144} className="w-36 h-36" />
@@ -43,36 +48,7 @@ export default async function SelfHostedWelcomePage() {
</CardTitle>
<CardDescription className="flex flex-col gap-4 text-center text-lg">
<p>Welcome to your own instance of TaxHacker. Let&apos;s set up a couple of settings to get started.</p>
<form action={selfHostedGetStartedAction} className="flex flex-col gap-8 pt-8">
<div>
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={config.ai.openaiApiKey} />
<small className="text-xs text-muted-foreground">
Get your API key from{" "}
<a
href="https://platform.openai.com/settings/organization/api-keys"
target="_blank"
className="underline"
>
OpenAI Platform Console
</a>
</small>
</div>
<div className="flex flex-row items-center justify-center gap-2">
<FormSelectCurrency
title="Default Currency"
name="default_currency"
defaultValue={DEFAULT_SETTINGS.find((s) => s.code === "default_currency")?.value ?? "EUR"}
currencies={DEFAULT_CURRENCIES}
/>
</div>
<Button type="submit" className="w-auto p-6">
Get Started
</Button>
</form>
<SelfHostedSetupFormClient defaultProvider={defaultProvider} defaultApiKeys={defaultApiKeys} />
</CardDescription>
</Card>
)

View File

@@ -0,0 +1,76 @@
"use client"
import { useState, useRef, useEffect, useCallback } from "react"
import { FormSelectCurrency } from "@/components/forms/select-currency"
import { FormInput } from "@/components/forms/simple"
import { Button } from "@/components/ui/button"
import { DEFAULT_CURRENCIES, DEFAULT_SETTINGS } from "@/models/defaults"
import { selfHostedGetStartedAction } from "../actions"
import { FormSelect } from "@/components/forms/simple"
import { PROVIDERS } from "@/lib/llm-providers"
type Props = {
defaultProvider: string
defaultApiKeys: Record<string, string>
}
export default function SelfHostedSetupFormClient({ defaultProvider, defaultApiKeys }: Props) {
const [provider, setProvider] = useState(defaultProvider)
const selected = PROVIDERS.find(p => p.key === provider)!
const getDefaultApiKey = useCallback((providerKey: string) => defaultApiKeys[providerKey] ?? "", [defaultApiKeys])
const [apiKey, setApiKey] = useState(getDefaultApiKey(provider))
const userTyped = useRef(false)
useEffect(() => {
if (!userTyped.current) {
setApiKey(getDefaultApiKey(provider))
}
userTyped.current = false
}, [provider, getDefaultApiKey])
return (
<form action={selfHostedGetStartedAction} className="flex flex-col gap-8 pt-8">
<div className="flex flex-row gap-4 items-center justify-center">
<FormSelect
title="LLM provider"
name="provider"
value={provider}
onValueChange={setProvider}
items={PROVIDERS.map(p => ({
code: p.key,
name: p.label,
logo: p.logo
}))}
/>
<FormSelectCurrency
title="Default Currency"
name="default_currency"
defaultValue={DEFAULT_SETTINGS.find((s) => s.code === "default_currency")?.value ?? "EUR"}
currencies={DEFAULT_CURRENCIES}
/>
</div>
<div>
<FormInput
title={`${selected.label} API Key`}
name={selected.apiKeyName}
value={apiKey ?? ""}
onChange={e => {
setApiKey(e.target.value)
userTyped.current = true
}}
placeholder={selected.placeholder}
/>
<small className="text-xs text-muted-foreground flex justify-center mt-2">
Get key from
{"\u00A0"}
<a href={selected.help.url} target="_blank" className="underline">
{selected.help.label}
</a>
</small>
</div>
<Button type="submit" className="w-auto p-6">
Get Started
</Button>
</form>
)
}

View File

@@ -1,4 +1,5 @@
"use client"
import Image from "next/image"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -87,9 +88,10 @@ export const FormSelect = ({
placeholder,
hideIfEmpty = false,
isRequired = false,
onValueChange,
...props
}: {
items: Array<{ code: string; name: string; color?: string; badge?: string }>
items: Array<{ code: string; name: string; color?: string; badge?: string; logo?: string }>
title?: string
emptyValue?: string
placeholder?: string
@@ -105,7 +107,11 @@ export const FormSelect = ({
return (
<span className="flex flex-col gap-1">
{title && <span className="text-sm font-medium">{title}</span>}
<Select {...props}>
<Select
value={props.value}
onValueChange={onValueChange}
{...props}
>
<SelectTrigger className={cn("w-full min-w-[150px] bg-background", isRequired && isEmpty && "bg-yellow-50")}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
@@ -114,6 +120,9 @@ export const FormSelect = ({
{items.map((item) => (
<SelectItem key={item.code} value={item.code}>
<div className="flex items-center gap-2 text-base pr-2">
{item.logo && (
<Image src={item.logo} alt={item.name} width={20} height={20} className="rounded-full" />
)}
{item.badge && <Badge className="px-2">{item.badge}</Badge>}
{!item.badge && item.color && (
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }} />

View File

@@ -3,44 +3,91 @@
import { fieldsToJsonSchema } from "@/ai/schema"
import { saveSettingsAction } from "@/app/(app)/settings/actions"
import { FormError } from "@/components/forms/error"
import { FormInput, FormTextarea } from "@/components/forms/simple"
import { FormTextarea } from "@/components/forms/simple"
import { Button } from "@/components/ui/button"
import { Card, CardTitle } from "@/components/ui/card"
import { Field } from "@/prisma/client"
import { CircleCheckBig, Edit } from "lucide-react"
import { CircleCheckBig, Edit, GripVertical } from "lucide-react"
import Link from "next/link"
import { useActionState } from "react"
import { useState, useActionState } from "react"
import {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors
} from "@dnd-kit/core"
import type { DragEndEvent } from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy
} from "@dnd-kit/sortable"
import { PROVIDERS } from "@/lib/llm-providers";
function getInitialProviderOrder(settings: Record<string, string>) {
let order: string[] = []
if (!settings.llm_providers) {
order = ['openai', 'google', 'mistral']
} else {
order = settings.llm_providers.split(",").map(p => p.trim())
}
// Remove duplicates and keep only valid providers
return order.filter((key, idx) => PROVIDERS.some(p => p.key === key) && order.indexOf(key) === idx)
}
export default function LLMSettingsForm({
settings,
fields,
showApiKey = true,
}: {
settings: Record<string, string>
fields: Field[]
showApiKey?: boolean
}) {
const [saveState, saveAction, pending] = useActionState(saveSettingsAction, null)
const [providerOrder, setProviderOrder] = useState<string[]>(getInitialProviderOrder(settings))
// Controlled values for each provider
const [providerValues, setProviderValues] = useState(() => {
const values: Record<string, { apiKey: string; model: string }> = {}
PROVIDERS.forEach((provider) => {
values[provider.key] = {
apiKey: settings[provider.apiKeyName],
model: settings[provider.modelName] || provider.defaultModelName,
}
})
return values
})
function handleProviderValueChange(providerKey: string, field: "apiKey" | "model", value: string) {
setProviderValues((prev) => ({
...prev,
[providerKey]: {
...prev[providerKey],
[field]: value,
},
}))
}
return (
<>
<form action={saveAction} className="space-y-4">
{showApiKey && (
<>
<FormInput title="OpenAI API Key" name="openai_api_key" defaultValue={settings.openai_api_key} />
<small className="text-muted-foreground">
Get your API key from{" "}
<a
href="https://platform.openai.com/settings/organization/api-keys"
target="_blank"
className="underline"
>
OpenAI Platform Console
</a>
</small>
</>
)}
<div className="space-y-2">
<label className="text-sm font-medium">LLM providers</label>
<DndProviderBlocks
providerOrder={providerOrder}
setProviderOrder={setProviderOrder}
providerValues={providerValues}
handleProviderValueChange={handleProviderValueChange}
/>
<small className="text-muted-foreground">
Drag provider blocks to reorder. First is highest priority.
</small>
</div>
<input type="hidden" name="llm_providers" value={providerOrder.join(",")} />
<FormTextarea
title="Prompt for File Analysis Form"
@@ -90,3 +137,106 @@ export default function LLMSettingsForm({
</>
)
}
type DndProviderBlocksProps = {
providerOrder: string[];
setProviderOrder: React.Dispatch<React.SetStateAction<string[]>>;
providerValues: Record<string, { apiKey: string; model: string }>;
handleProviderValueChange: (providerKey: string, field: "apiKey" | "model", value: string) => void;
};
function DndProviderBlocks({ providerOrder, setProviderOrder, providerValues, handleProviderValueChange }: DndProviderBlocksProps) {
const sensors = useSensors(useSensor(PointerSensor))
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (!over || active.id === over.id) return
const oldIndex = providerOrder.indexOf(active.id as string)
const newIndex = providerOrder.indexOf(over.id as string)
setProviderOrder(arrayMove(providerOrder, oldIndex, newIndex))
}
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={providerOrder} strategy={verticalListSortingStrategy}>
{providerOrder.map((providerKey, idx) => (
<SortableProviderBlock
key={providerKey}
id={providerKey}
idx={idx}
providerKey={providerKey}
value={providerValues[providerKey]}
handleValueChange={handleProviderValueChange}
/>
))}
</SortableContext>
</DndContext>
)
}
type SortableProviderBlockProps = {
id: string;
idx: number;
providerKey: string;
value: { apiKey: string; model: string };
handleValueChange: (providerKey: string, field: "apiKey" | "model", value: string) => void;
};
function SortableProviderBlock({ id, idx, providerKey, value, handleValueChange }: SortableProviderBlockProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
const provider = PROVIDERS.find(p => p.key === providerKey)
if (!provider) return null
return (
<div
ref={setNodeRef}
style={{
transform: transform ? `translateY(${transform.y}px)` : undefined,
transition,
opacity: isDragging ? 0.6 : 1,
}}
className={`bg-muted rounded-lg p-4 shadow flex flex-col gap-2 mb-2`}
>
<div className="flex flex-row items-center gap-2 mb-2">
{/* Drag handle */}
<span
{...attributes}
{...listeners}
className="cursor-grab p-1 rounded hover:bg-accent transition inline-flex items-center"
aria-label="Drag to reorder"
>
<GripVertical className="w-5 h-5 text-muted-foreground" />
</span>
<span className="font-semibold">{provider.label}</span>
</div>
<div className="flex flex-row gap-4 items-center">
<input
type="text"
name={provider.apiKeyName}
value={value.apiKey}
onChange={e => handleValueChange(provider.key, "apiKey", e.target.value)}
className="flex-1 border rounded px-2 py-1"
placeholder="API key"
/>
<input
type="text"
name={provider.modelName}
value={value.model}
onChange={e => handleValueChange(provider.key, "model", e.target.value)}
className="flex-1 border rounded px-2 py-1"
placeholder="Model name"
/>
</div>
{provider.apiDoc && (
<small className="text-muted-foreground">
Get your API key from{" "}
<a
href={provider.apiDoc}
target="_blank"
className="underline"
>
{provider.apiDocLabel}
</a>
</small>
)}
</div>
)
}

View File

@@ -21,6 +21,7 @@ services:
postgres:
image: postgres:17-alpine
container_name: taxhacker-postgres
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres

View File

@@ -7,6 +7,12 @@ export const settingsFormSchema = z.object({
default_category: z.string().optional(),
default_project: z.string().optional(),
openai_api_key: z.string().optional(),
openai_model_name: z.string().default('gpt-4o-mini'),
google_api_key: z.string().optional(),
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'),
prompt_analyse_new_file: z.string().optional(),
is_welcome_message_hidden: z.string().optional(),
})

View File

@@ -6,6 +6,10 @@ const envSchema = z.object({
SELF_HOSTED_MODE: z.enum(["true", "false"]).default("true"),
OPENAI_API_KEY: z.string().optional(),
OPENAI_MODEL_NAME: z.string().default("gpt-4o-mini"),
GOOGLE_API_KEY: z.string().optional(),
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"),
BETTER_AUTH_SECRET: z
.string()
.min(16, "Auth secret must be at least 16 characters")
@@ -50,7 +54,11 @@ const config = {
},
ai: {
openaiApiKey: env.OPENAI_API_KEY,
modelName: env.OPENAI_MODEL_NAME,
openaiModelName: env.OPENAI_MODEL_NAME,
googleApiKey: env.GOOGLE_API_KEY,
googleModelName: env.GOOGLE_MODEL_NAME,
mistralApiKey: env.MISTRAL_API_KEY,
mistralModelName: env.MISTRAL_MODEL_NAME,
},
auth: {
secret: env.BETTER_AUTH_SECRET,

47
lib/llm-providers.ts Normal file
View File

@@ -0,0 +1,47 @@
export const PROVIDERS = [
{
key: "openai",
label: "OpenAI",
apiKeyName: "openai_api_key",
modelName: "openai_model_name",
defaultModelName: "gpt-4o-mini",
apiDoc: "https://platform.openai.com/settings/organization/api-keys",
apiDocLabel: "OpenAI Platform Console",
placeholder: "sk-...",
help: {
url: "https://platform.openai.com/settings/organization/api-keys",
label: "OpenAI Platform Console"
},
logo: "/logo/openai.svg"
},
{
key: "google",
label: "Google",
apiKeyName: "google_api_key",
modelName: "google_model_name",
defaultModelName: "gemini-2.5-flash",
apiDoc: "https://aistudio.google.com/apikey",
apiDocLabel: "Google AI Studio",
placeholder: "...",
help: {
url: "https://aistudio.google.com/apikey",
label: "Google AI Studio"
},
logo: "/logo/google.svg"
},
{
key: "mistral",
label: "Mistral",
apiKeyName: "mistral_api_key",
modelName: "mistral_model_name",
defaultModelName: "mistral-medium-latest",
apiDoc: "https://admin.mistral.ai/organization/api-keys",
apiDocLabel: "Mistral Admin Console",
placeholder: "...",
help: {
url: "https://admin.mistral.ai/organization/api-keys",
label: "Mistral Admin Console"
},
logo: "/logo/mistral.svg"
},
]

View File

@@ -1,12 +1,51 @@
import { prisma } from "@/lib/db"
import { PROVIDERS } from "@/lib/llm-providers"
import { cache } from "react"
import { LLMProvider } from "@/ai/providers/llmProvider"
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 providers = priorities.map((provider) => {
if (provider === "openai") {
return {
provider: provider as LLMProvider,
apiKey: settings.openai_api_key || "",
model: settings.openai_model_name || PROVIDERS[0]['defaultModelName'],
}
}
if (provider === "google") {
return {
provider: provider as LLMProvider,
apiKey: settings.google_api_key || "",
model: settings.google_model_name || PROVIDERS[1]['defaultModelName'],
}
}
if (provider === "mistral") {
return {
provider: provider as LLMProvider,
apiKey: settings.mistral_api_key || "",
model: settings.mistral_model_name || PROVIDERS[2]['defaultModelName'],
}
}
return null
}).filter((provider): provider is NonNullable<typeof provider> => provider !== null)
return {
providers,
}
}
export const getSettings = cache(async (userId: string): Promise<SettingsMap> => {
const settings = await prisma.setting.findMany({
where: { userId },
})
return settings.reduce((acc, setting) => {
acc[setting.code] = setting.value || ""
return acc

661
package-lock.json generated
View File

@@ -8,8 +8,13 @@
"name": "taxhacker",
"version": "0.5.5",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@fast-csv/format": "^5.0.2",
"@fast-csv/parse": "^5.0.2",
"@langchain/google-genai": "^0.2.14",
"@langchain/mistralai": "^0.2.1",
"@langchain/openai": "^0.6.1",
"@prisma/client": "^6.6.0",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
@@ -31,11 +36,11 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"jszip": "^3.10.1",
"langchain": "^0.3.30",
"lucide-react": "^0.475.0",
"mime-types": "^3.0.1",
"next": "^15.2.4",
"next-themes": "^0.4.4",
"openai": "^4.85.4",
"pdf2pic": "^3.1.4",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",
@@ -364,6 +369,7 @@
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.5.tgz",
"integrity": "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==",
"license": "MIT",
"dependencies": {
"typescript": "^5.8.2",
"uncrypto": "^0.1.3"
@@ -374,6 +380,66 @@
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz",
"integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="
},
"node_modules/@cfworker/json-schema": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
"license": "MIT",
"peer": true
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -1035,6 +1101,15 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
@@ -1544,12 +1619,185 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@langchain/core": {
"version": "0.3.63",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.63.tgz",
"integrity": "sha512-CQfyu4WgwizUhSc1YsDDzzHga6WVhLqeuAyCD4VpGAPa3k3QI+H0b3ECFr/WjJMw0amMtHtfgPWMa1tS7P7qVg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@cfworker/json-schema": "^4.0.2",
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
"js-tiktoken": "^1.0.12",
"langsmith": "^0.3.33",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",
"p-retry": "4",
"uuid": "^10.0.0",
"zod": "^3.25.32",
"zod-to-json-schema": "^3.22.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@langchain/core/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@langchain/core/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"peer": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@langchain/google-genai": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-0.2.14.tgz",
"integrity": "sha512-gKe/T2LNh8wSSMJOaFmYd8cwQnDSXKtVtC6a7CFoq5nWuh0bKzhItM/7bue1aMN8mlKfB2G1HCwxhaZoSpS/DA==",
"license": "MIT",
"dependencies": {
"@google/generative-ai": "^0.24.0",
"uuid": "^11.1.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": ">=0.3.58 <0.4.0"
}
},
"node_modules/@langchain/google-genai/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/@langchain/mistralai": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@langchain/mistralai/-/mistralai-0.2.1.tgz",
"integrity": "sha512-s91BlNcuxaaZGnVukyl81nwGrWpeE0EYiAdEFoBmZwlT4yLpx+QpPhRsGKrTg/Vm7Nscy6Wd8Xy2PJ93wftMdw==",
"license": "MIT",
"dependencies": {
"@mistralai/mistralai": "^1.3.1",
"uuid": "^10.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": ">=0.3.58 <0.4.0"
}
},
"node_modules/@langchain/mistralai/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@langchain/openai": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.6.1.tgz",
"integrity": "sha512-jm8MzMEjAKPReYma4Lewb9vGnocKbhoClqPuRTxtKPDgqQ5yJWSisNy4iZO/a1d6ag/7MnxwKMjVsJdy1cBsxw==",
"license": "MIT",
"dependencies": {
"js-tiktoken": "^1.0.12",
"openai": "^5.3.0",
"zod": "^3.25.32"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": ">=0.3.58 <0.4.0"
}
},
"node_modules/@langchain/openai/node_modules/openai": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.9.2.tgz",
"integrity": "sha512-d7t/lRkwZpSwIk7GW3EHRSGAlsuoi1WL6VhCO02raEzZO2ahEVAbWn3WmOcpeh9zF6xF9weJXoVeDkWJRz+SHA==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/@langchain/textsplitters": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz",
"integrity": "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==",
"license": "MIT",
"dependencies": {
"js-tiktoken": "^1.0.12"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": ">=0.2.21 <0.4.0"
}
},
"node_modules/@levischuck/tiny-cbor": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
"license": "MIT"
},
"node_modules/@mistralai/mistralai": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.7.4.tgz",
"integrity": "sha512-wty9hHEvIJ5RS8+75NY+a1zXtCCqYQgI26e8R2N7O9ZvD16ep2kF6ciJD2EiWOgS/K+iycRGsrI3nqgkPLG/Xw==",
"dependencies": {
"zod-to-json-schema": "^3.24.1"
},
"peerDependencies": {
"zod": ">= 3"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz",
@@ -4124,9 +4372,9 @@
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -4638,6 +4886,8 @@
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
@@ -4683,6 +4933,12 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT"
},
"node_modules/@types/sharp": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz",
@@ -4707,6 +4963,12 @@
"@types/node": "*"
}
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz",
@@ -4846,9 +5108,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5280,6 +5542,8 @@
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"event-target-shim": "^5.0.0"
},
@@ -5341,6 +5605,8 @@
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"humanize-ms": "^1.2.1"
},
@@ -5463,7 +5729,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
@@ -5693,7 +5958,9 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
@@ -5758,9 +6025,10 @@
"license": "MIT"
},
"node_modules/better-auth": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.2.10.tgz",
"integrity": "sha512-nEj1RG4DdLUuJiV5CR93ORyPCptGRBwksaPPCkUtGo9ka+UIlTpaiKoTaTqVLLYlqwX4bOj9tJ32oBNdf2G3Kg==",
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.2.12.tgz",
"integrity": "sha512-YicCyjQ+lxb7YnnaCewrVOjj3nPVa0xcfrOJK7k5MLMX9Mt9UnJ8GYaVQNHOHLyVxl92qc3C758X1ihqAUzm4w==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "0.2.5",
"@better-fetch/fetch": "^1.1.18",
@@ -5770,16 +6038,16 @@
"@simplewebauthn/server": "^13.0.0",
"better-call": "^1.0.8",
"defu": "^6.1.4",
"jose": "^5.9.6",
"jose": "^6.0.11",
"kysely": "^0.28.2",
"nanostores": "^0.11.3",
"zod": "^3.24.1"
}
},
"node_modules/better-call": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.11.tgz",
"integrity": "sha512-MOM01EMZFMzApWq9+WfqAnl2+DzFoMNp4H+lTFE1p7WF4evMeaQAAcOhI1WwMjITV4PGIWJ3Vn5GciQ5VHXbIA==",
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.12.tgz",
"integrity": "sha512-ssq5OfB9Ungv2M1WVrRnMBomB0qz1VKuhkY2WxjHaLtlsHoSe9EPolj1xf7xf8LY9o3vfk3Rx6rCWI4oVHeBRg==",
"dependencies": {
"@better-fetch/fetch": "^1.1.4",
"rou3": "^0.5.1",
@@ -5809,9 +6077,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5957,6 +6225,19 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -5990,7 +6271,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -6137,6 +6417,8 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -6166,6 +6448,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/console-table-printer": {
"version": "2.14.6",
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz",
"integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==",
"license": "MIT",
"dependencies": {
"simple-wcswidth": "^1.0.1"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -6305,6 +6596,16 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -6368,6 +6669,8 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.4.0"
}
@@ -6678,6 +6981,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -7231,10 +7535,18 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -7436,6 +7748,8 @@
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -7450,13 +7764,17 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -7466,6 +7784,8 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -7478,6 +7798,8 @@
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
@@ -7678,9 +8000,9 @@
"peer": true
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -7849,6 +8171,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -7949,6 +8272,8 @@
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"ms": "^2.0.0"
}
@@ -8556,14 +8881,23 @@
}
},
"node_modules/jose": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz",
"integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tiktoken": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.20.tgz",
"integrity": "sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.5.1"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8574,7 +8908,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -8636,6 +8969,15 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -8678,10 +9020,167 @@
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.2.tgz",
"integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/langchain": {
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.30.tgz",
"integrity": "sha512-UyVsfwHDpHbrnWrjWuhJHqi8Non+Zcsf2kdpDTqyJF8NXrHBOpjdHT5LvPuW9fnE7miDTWf5mLcrWAGZgcrznQ==",
"license": "MIT",
"dependencies": {
"@langchain/openai": ">=0.1.0 <0.7.0",
"@langchain/textsplitters": ">=0.0.0 <0.2.0",
"js-tiktoken": "^1.0.12",
"js-yaml": "^4.1.0",
"jsonpointer": "^5.0.1",
"langsmith": "^0.3.33",
"openapi-types": "^12.1.3",
"p-retry": "4",
"uuid": "^10.0.0",
"yaml": "^2.2.1",
"zod": "^3.25.32"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/anthropic": "*",
"@langchain/aws": "*",
"@langchain/cerebras": "*",
"@langchain/cohere": "*",
"@langchain/core": ">=0.3.58 <0.4.0",
"@langchain/deepseek": "*",
"@langchain/google-genai": "*",
"@langchain/google-vertexai": "*",
"@langchain/google-vertexai-web": "*",
"@langchain/groq": "*",
"@langchain/mistralai": "*",
"@langchain/ollama": "*",
"@langchain/xai": "*",
"axios": "*",
"cheerio": "*",
"handlebars": "^4.7.8",
"peggy": "^3.0.2",
"typeorm": "*"
},
"peerDependenciesMeta": {
"@langchain/anthropic": {
"optional": true
},
"@langchain/aws": {
"optional": true
},
"@langchain/cerebras": {
"optional": true
},
"@langchain/cohere": {
"optional": true
},
"@langchain/deepseek": {
"optional": true
},
"@langchain/google-genai": {
"optional": true
},
"@langchain/google-vertexai": {
"optional": true
},
"@langchain/google-vertexai-web": {
"optional": true
},
"@langchain/groq": {
"optional": true
},
"@langchain/mistralai": {
"optional": true
},
"@langchain/ollama": {
"optional": true
},
"@langchain/xai": {
"optional": true
},
"axios": {
"optional": true
},
"cheerio": {
"optional": true
},
"handlebars": {
"optional": true
},
"peggy": {
"optional": true
},
"typeorm": {
"optional": true
}
}
},
"node_modules/langchain/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/langsmith": {
"version": "0.3.46",
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.46.tgz",
"integrity": "sha512-Hhi4/cMjhWIGpu0DW5eQrXBbeeKQWPYYQyJCYzhFjod+xinMry4i8QR0gxrrgjGOgfMuU6nyK79YqjGTEPVbDA==",
"license": "MIT",
"dependencies": {
"@types/uuid": "^10.0.0",
"chalk": "^4.1.2",
"console-table-printer": "^2.12.1",
"p-queue": "^6.6.2",
"p-retry": "4",
"semver": "^7.6.3",
"uuid": "^10.0.0"
},
"peerDependencies": {
"@opentelemetry/api": "*",
"@opentelemetry/exporter-trace-otlp-proto": "*",
"@opentelemetry/sdk-trace-base": "*",
"openai": "*"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@opentelemetry/exporter-trace-otlp-proto": {
"optional": true
},
"@opentelemetry/sdk-trace-base": {
"optional": true
},
"openai": {
"optional": true
}
}
},
"node_modules/langsmith/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -8997,6 +9496,16 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"license": "MIT",
"peer": true,
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -9162,6 +9671,8 @@
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=10.5.0"
}
@@ -9345,6 +9856,8 @@
"resolved": "https://registry.npmjs.org/openai/-/openai-4.89.0.tgz",
"integrity": "sha512-XNI0q2l8/Os6jmojxaID5EhyQjxZgzR2gWcpEjYWK5hGKwE7AcifxEY7UNwFDDHJQXqeiosQ0CJwQN+rvnwdjA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
@@ -9375,6 +9888,8 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.81.tgz",
"integrity": "sha512-7KO9oZ2//ivtSsryp0LQUqq79zyGXzwq1WqfywpC9ucjY7YyltMMmxWgtRFRKCxwa7VPxVBVy4kHf5UC1E8Lug==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -9383,6 +9898,14 @@
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT"
},
"node_modules/optionator": {
@@ -9421,6 +9944,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -9451,6 +9983,47 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"license": "MIT",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -10283,6 +10856,15 @@
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -10773,6 +11355,12 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-wcswidth": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
"license": "MIT"
},
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
@@ -11777,6 +12365,8 @@
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14"
}
@@ -12161,13 +12751,22 @@
"license": "MIT"
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}
}
}
}

View File

@@ -10,8 +10,13 @@
"lint": "next lint"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@fast-csv/format": "^5.0.2",
"@fast-csv/parse": "^5.0.2",
"@langchain/google-genai": "^0.2.14",
"@langchain/mistralai": "^0.2.1",
"@langchain/openai": "^0.6.1",
"@prisma/client": "^6.6.0",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
@@ -33,11 +38,11 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"jszip": "^3.10.1",
"langchain": "^0.3.30",
"lucide-react": "^0.475.0",
"mime-types": "^3.0.1",
"next": "^15.2.4",
"next-themes": "^0.4.4",
"openai": "^4.85.4",
"pdf2pic": "^3.1.4",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",

1
public/logo/google.svg Normal file
View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><defs><linearGradient id="lobe-icons-gemini-fill" x1="0%" x2="68.73%" y1="100%" y2="30.395%"><stop offset="0%" stop-color="#1C7DFF"></stop><stop offset="52.021%" stop-color="#1C69FF"></stop><stop offset="100%" stop-color="#F0DCD6"></stop></linearGradient></defs><path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12" fill="url(#lobe-icons-gemini-fill)" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 570 B

1
public/logo/mistral.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 0 256 233"><path d="M186.18182 0h46.54545v46.54545h-46.54545z"/><path fill="#F7D046" d="M209.45454 0h46.54545v46.54545h-46.54545z"/><path d="M0 0h46.54545v46.54545H0zM0 46.54545h46.54545V93.0909H0zM0 93.09091h46.54545v46.54545H0zM0 139.63636h46.54545v46.54545H0zM0 186.18182h46.54545v46.54545H0z"/><path fill="#F7D046" d="M23.27273 0h46.54545v46.54545H23.27273z"/><path fill="#F2A73B" d="M209.45454 46.54545h46.54545V93.0909h-46.54545zM23.27273 46.54545h46.54545V93.0909H23.27273z"/><path d="M139.63636 46.54545h46.54545V93.0909h-46.54545z"/><path fill="#F2A73B" d="M162.90909 46.54545h46.54545V93.0909h-46.54545zM69.81818 46.54545h46.54545V93.0909H69.81818z"/><path fill="#EE792F" d="M116.36364 93.09091h46.54545v46.54545h-46.54545zM162.90909 93.09091h46.54545v46.54545h-46.54545zM69.81818 93.09091h46.54545v46.54545H69.81818z"/><path d="M93.09091 139.63636h46.54545v46.54545H93.09091z"/><path fill="#EB5829" d="M116.36364 139.63636h46.54545v46.54545h-46.54545z"/><path fill="#EE792F" d="M209.45454 93.09091h46.54545v46.54545h-46.54545zM23.27273 93.09091h46.54545v46.54545H23.27273z"/><path d="M186.18182 139.63636h46.54545v46.54545h-46.54545z"/><path fill="#EB5829" d="M209.45454 139.63636h46.54545v46.54545h-46.54545z"/><path d="M186.18182 186.18182h46.54545v46.54545h-46.54545z"/><path fill="#EB5829" d="M23.27273 139.63636h46.54545v46.54545H23.27273z"/><path fill="#EA3326" d="M209.45454 186.18182h46.54545v46.54545h-46.54545zM23.27273 186.18182h46.54545v46.54545H23.27273z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/logo/openai.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="260" preserveAspectRatio="xMidYMid" viewBox="0 0 256 260"><path d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB