Merge pull request #56 from presenton/feat/ollama_api

fix(fastapi, nextjs): fixes errors while using Custom Ollama URL
This commit is contained in:
Saurav Niraula 2025-07-06 15:30:06 +05:45 committed by GitHub
commit 39d5a130ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 223 additions and 106 deletions

View file

@ -1,5 +1,25 @@
services:
production:
# image: ghcr.io/presenton/presenton:latest
build:
context: .
dockerfile: Dockerfile
ports:
# You can replace 5000 with any other port number of your choice to run Presenton on a different port number.
- "5000:80"
volumes:
- ./user_data:/app/user_data
environment:
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
- LLM_PROVIDER_URL=${LLM_PROVIDER_URL}
- LLM_API_KEY=${LLM_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- PEXELS_API_KEY=${PEXELS_API_KEY}
production-gpu:
# image: ghcr.io/presenton/presenton:latest
build:
context: .
@ -19,12 +39,35 @@ services:
environment:
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
- LLM_PROVIDER_URL=${LLM_PROVIDER_URL}
- LLM_API_KEY=${LLM_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- PEXELS_API_KEY=${PEXELS_API_KEY}
development:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "5000:80"
- "3000:3000"
- "8000:8000"
volumes:
- .:/app
environment:
- NODE_ENV=development
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
- LLM_PROVIDER_URL=${LLM_PROVIDER_URL}
- LLM_API_KEY=${LLM_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- PEXELS_API_KEY=${PEXELS_API_KEY}
development-gpu:
build:
context: .
dockerfile: Dockerfile.dev
@ -45,6 +88,8 @@ services:
- NODE_ENV=development
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
- LLM_PROVIDER_URL=${LLM_PROVIDER_URL}
- LLM_API_KEY=${LLM_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- OLLAMA_MODEL=${OLLAMA_MODEL}

View file

@ -93,7 +93,7 @@ class PresentationEditHandler:
icons=None,
presentation=slide_to_edit.presentation,
properties=slide_to_edit.properties,
content=edited_content,
content=edited_content.to_content(),
)
new_slide_images_count = new_slide_model.images_count

View file

@ -1,9 +1,12 @@
import os
import aiohttp
from fastapi import HTTPException
from api.models import LogMetadata
from api.routers.presentation.models import OllamaModelStatusResponse
from api.services.logging import LoggingService
from api.utils.model_utils import get_llm_api_key_or, get_llm_provider_url_or
from api.utils.model_utils import (
get_llm_provider_url_or,
get_ollama_request_headers,
)
class ListPulledOllamaModelsHandler:
@ -13,13 +16,23 @@ class ListPulledOllamaModelsHandler:
logging_service.message("Listing Ollama models"),
extra=log_metadata.model_dump(),
)
async with aiohttp.ClientSession() as session:
async with session.get(
f"{get_llm_provider_url_or()}/api/tags",
headers={"Authorization": f"Bearer {get_llm_api_key_or()}"},
headers=get_ollama_request_headers(),
) as response:
response_data = await response.json()
if response.status == 200:
response_data = await response.json()
elif response.status == 403:
raise HTTPException(
status_code=403,
detail="Forbidden: Please check your Ollama Configuration",
)
else:
raise HTTPException(
status_code=response.status,
detail=f"Failed to list Ollama models: {response.status}",
)
logging_service.logger.info(
logging_service.message(response_data),

View file

@ -1,4 +1,5 @@
import json
import traceback
import aiohttp
from fastapi import BackgroundTasks, HTTPException
from api.models import LogMetadata
@ -8,7 +9,10 @@ from api.routers.presentation.handlers.list_supported_ollama_models import (
from api.routers.presentation.models import OllamaModelStatusResponse
from api.services.instances import REDIS_SERVICE
from api.services.logging import LoggingService
from api.utils.model_utils import get_llm_api_key_or, get_llm_provider_url_or
from api.utils.model_utils import (
get_llm_provider_url_or,
get_ollama_request_headers,
)
class PullOllamaModelHandler:
@ -38,7 +42,7 @@ class PullOllamaModelHandler:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{get_llm_provider_url_or()}/api/tags",
headers={"Authorization": f"Bearer {get_llm_api_key_or()}"},
headers=get_ollama_request_headers(),
) as response:
if response.status == 200:
pulled_models = await response.json()
@ -57,17 +61,49 @@ class PullOllamaModelHandler:
downloaded=filtered_models[0]["size"],
done=True,
)
elif response.status == 403:
print(response)
raise HTTPException(
status_code=403,
detail="Forbidden: Please check your Ollama Configuration",
)
else:
raise HTTPException(
status_code=response.status,
detail=f"Failed to list Ollama models: {response.status}",
)
except HTTPException as e:
logging_service.logger.warning(
logging_service.message(e.detail),
extra=log_metadata.model_dump(),
)
raise e
except Exception as e:
traceback.print_exc()
logging_service.logger.warning(
f"Failed to check pulled models: {e}",
extra=log_metadata.model_dump(),
)
raise HTTPException(
status_code=500,
detail=f"Failed to check pulled models: {e}",
)
saved_model_status = REDIS_SERVICE.get(f"ollama_models/{self.name}")
# If the model is being pulled, return the model
if saved_model_status:
return json.loads(saved_model_status)
saved_model_status_json = json.loads(saved_model_status)
# If the model is being pulled, return the model
# ? If the model status is pulled in redis but was not found while listing pulled models,
# ? it means the model was deleted and we need to pull it again
if (
saved_model_status_json["status"] == "error"
or saved_model_status_json["status"] == "pulled"
):
REDIS_SERVICE.delete(f"ollama_models/{self.name}")
else:
return saved_model_status_json
# If the model is not being pulled, pull the model
background_tasks.add_task(self.pull_model_in_background)
@ -93,8 +129,8 @@ class PullOllamaModelHandler:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{get_llm_provider_url_or()}/api/pull",
headers=get_ollama_request_headers(),
json={"model": self.name},
headers={"Authorization": f"Bearer {get_llm_api_key_or()}"},
) as response:
if response.status != 200:
raise HTTPException(
@ -136,7 +172,10 @@ class PullOllamaModelHandler:
f"ollama_models/{self.name}",
json.dumps(saved_model_status.model_dump(mode="json")),
)
raise e
raise HTTPException(
status_code=500,
detail=f"Failed to pull model: {e}",
)
saved_model_status.done = True
saved_model_status.status = "pulled"

View file

@ -10,11 +10,18 @@ def is_ollama_selected() -> bool:
def get_llm_provider_url_or():
return os.getenv("LLM_PROVIDER_URL") or "http://localhost:11434"
llm_provider_url = os.getenv("LLM_PROVIDER_URL") or "http://localhost:11434"
if llm_provider_url.endswith("/"):
return llm_provider_url[:-1]
return llm_provider_url
def get_llm_api_key_or():
return os.getenv("LLM_API_KEY") or "ollama"
def get_ollama_request_headers():
if os.getenv("LLM_API_KEY"):
return {
"Authorization": f"Bearer {os.getenv('LLM_API_KEY')}",
}
return {}
def get_selected_llm_provider() -> SelectedLLMProvider:
@ -41,7 +48,7 @@ def get_llm_api_key():
elif selected_llm == SelectedLLMProvider.GOOGLE:
return os.getenv("GOOGLE_API_KEY")
elif selected_llm == SelectedLLMProvider.OLLAMA:
return get_llm_api_key_or()
return os.getenv("LLM_API_KEY") or "ollama"
else:
raise ValueError(f"Invalid LLM API key")

View file

@ -42,13 +42,13 @@ def get_user_config():
return UserConfig(
LLM=existing_config.LLM or os.getenv("LLM"),
LLM_PROVIDER_URL=existing_config.LLM_PROVIDER_URL
or os.getenv("LLM_PROVIDER_URL"),
LLM_API_KEY=existing_config.LLM_API_KEY or os.getenv("LLM_API_KEY"),
OPENAI_API_KEY=existing_config.OPENAI_API_KEY or os.getenv("OPENAI_API_KEY"),
GOOGLE_API_KEY=existing_config.GOOGLE_API_KEY or os.getenv("GOOGLE_API_KEY"),
MODEL=existing_config.MODEL or os.getenv("MODEL"),
PEXELS_API_KEY=existing_config.PEXELS_API_KEY or os.getenv("PEXELS_API_KEY"),
LLM_PROVIDER_URL=existing_config.LLM_PROVIDER_URL
or os.getenv("LLM_PROVIDER_URL"),
LLM_API_KEY=existing_config.LLM_API_KEY or os.getenv("LLM_API_KEY"),
)
@ -56,6 +56,10 @@ def update_env_with_user_config():
user_config = get_user_config()
if user_config.LLM:
os.environ["LLM"] = user_config.LLM
if user_config.LLM_PROVIDER_URL:
os.environ["LLM_PROVIDER_URL"] = user_config.LLM_PROVIDER_URL
if user_config.LLM_API_KEY:
os.environ["LLM_API_KEY"] = user_config.LLM_API_KEY
if user_config.OPENAI_API_KEY:
os.environ["OPENAI_API_KEY"] = user_config.OPENAI_API_KEY
if user_config.GOOGLE_API_KEY:
@ -64,10 +68,6 @@ def update_env_with_user_config():
os.environ["MODEL"] = user_config.MODEL
if user_config.PEXELS_API_KEY:
os.environ["PEXELS_API_KEY"] = user_config.PEXELS_API_KEY
if user_config.LLM_PROVIDER_URL:
os.environ["LLM_PROVIDER_URL"] = user_config.LLM_PROVIDER_URL
if user_config.LLM_API_KEY:
os.environ["LLM_API_KEY"] = user_config.LLM_API_KEY
def get_resource(relative_path):

View file

@ -2,11 +2,6 @@ import os
import uvicorn
import argparse
from api.main import app
# ? Helps pyinstaller for dependencies resolution
app
if __name__ == "__main__":
os.makedirs("debug", exist_ok=True)

View file

@ -34,12 +34,13 @@ export async function POST(request: Request) {
}
const mergedConfig: LLMConfig = {
LLM: userConfig.LLM || existingConfig.LLM,
LLM_PROVIDER_URL: userConfig.LLM_PROVIDER_URL || existingConfig.LLM_PROVIDER_URL,
LLM_API_KEY: userConfig.LLM_API_KEY,
OPENAI_API_KEY: userConfig.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY,
GOOGLE_API_KEY: userConfig.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY,
MODEL: userConfig.MODEL || existingConfig.MODEL,
LLM_PROVIDER_URL: userConfig.LLM_PROVIDER_URL || existingConfig.LLM_PROVIDER_URL,
LLM_API_KEY: userConfig.LLM_API_KEY || existingConfig.LLM_API_KEY,
PEXELS_API_KEY: userConfig.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,
USE_CUSTOM_URL: userConfig.USE_CUSTOM_URL === undefined ? existingConfig.USE_CUSTOM_URL : userConfig.USE_CUSTOM_URL,
}
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig))
return NextResponse.json(mergedConfig)

View file

@ -72,7 +72,7 @@ const SettingsPage = () => {
});
const [isLoading, setIsLoading] = useState<boolean>(false);
const [openModelSelect, setOpenModelSelect] = useState(false);
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(false);
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(userConfigState.llm_config.USE_CUSTOM_URL || false);
const api_key_changed = (apiKey: string, field?: string) => {
if (llmConfig.LLM === 'openai') {
@ -91,27 +91,12 @@ const SettingsPage = () => {
}
const handleSaveConfig = async () => {
if (llmConfig.LLM === 'ollama') {
try {
try {
await handleSaveLLMConfig(llmConfig);
if (llmConfig.LLM === 'ollama') {
setIsLoading(true);
await pullOllamaModels();
toast({
title: 'Success',
description: 'Model downloaded successfully',
});
} catch (error) {
console.error('Error pulling model:', error);
toast({
title: 'Error',
description: 'Failed to download model. Please try again.',
variant: 'destructive',
});
setIsLoading(false);
return;
}
}
try {
await handleSaveLLMConfig(llmConfig, useCustomOllamaUrl);
toast({
title: 'Success',
description: 'Configuration saved successfully',
@ -122,7 +107,7 @@ const SettingsPage = () => {
console.error('Error:', error);
toast({
title: 'Error',
description: 'Failed to save configuration',
description: error instanceof Error ? error.message : 'Failed to save configuration',
variant: 'destructive',
});
setIsLoading(false);
@ -136,34 +121,48 @@ const SettingsPage = () => {
}
}
const resetDownloadingModel = () => {
setDownloadingModel({
name: '',
size: null,
downloaded: null,
status: '',
done: false,
});
}
const pullOllamaModels = async (): Promise<void> => {
return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
try {
const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.MODEL}`);
if (response.status === 200) {
const data = await response.json();
if (data.done) {
if (data.done && data.status !== 'error') {
clearInterval(interval);
setDownloadingModel(data);
resolve();
} else if (data.status === 'error') {
clearInterval(interval);
resetDownloadingModel();
reject(new Error('Error occurred while pulling model'));
} else {
setDownloadingModel(data);
}
} else {
clearInterval(interval);
reject(new Error('Model pulling failed'));
resetDownloadingModel();
if (response.status === 403) {
reject(new Error('Request to Ollama Not Authorized'));
}
reject(new Error('Error occurred while pulling model'));
}
} catch (error) {
console.log('Error fetching ollama models:', error);
clearInterval(interval);
resetDownloadingModel();
reject(error);
}
}, 1000);
});
}
@ -177,6 +176,14 @@ const SettingsPage = () => {
}
}
const setOllamaConfig = () => {
if (!useCustomOllamaUrl) {
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: 'http://localhost:11434', LLM_API_KEY: undefined, USE_CUSTOM_URL: false });
} else {
setLlmConfig({ ...llmConfig, USE_CUSTOM_URL: true });
}
}
useEffect(() => {
if (!canChangeKeys) {
@ -188,11 +195,7 @@ const SettingsPage = () => {
}, [userConfigState.llm_config.LLM]);
useEffect(() => {
if (!useCustomOllamaUrl) {
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: undefined, LLM_API_KEY: undefined });
} else {
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: 'http://localhost:11434', LLM_API_KEY: '' });
}
setOllamaConfig();
}, [useCustomOllamaUrl]);
if (!canChangeKeys) {

View file

@ -41,7 +41,7 @@ export function StoreInitializer({ children }: { children: React.ReactNode }) {
llmConfig.LLM = 'openai';
}
dispatch(setLLMConfig(llmConfig));
const isValid = hasValidLLMConfig(llmConfig, false);
const isValid = hasValidLLMConfig(llmConfig);
if (isValid) {
// Check if the selected Ollama model is pulled
if (llmConfig.LLM === 'ollama') {
@ -75,10 +75,15 @@ export function StoreInitializer({ children }: { children: React.ReactNode }) {
}
const checkIfSelectedOllamaModelIsPulled = async (ollamaModel: string) => {
const response = await fetch('/api/v1/ppt/ollama/list-pulled-models');
const data = await response.json();
const pulledModels = data.map((model: any) => model.name);
return pulledModels.includes(ollamaModel);
try {
const response = await fetch('/api/v1/ppt/ollama/list-pulled-models');
const data = await response.json();
const pulledModels = data.map((model: any) => model.name);
return pulledModels.includes(ollamaModel);
} catch (error) {
console.error('Error checking if selected Ollama model is pulled:', error);
return false;
}
}

View file

@ -159,7 +159,7 @@ export default function Home() {
});
const [isLoading, setIsLoading] = useState<boolean>(false);
const [openModelSelect, setOpenModelSelect] = useState(false);
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(false);
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(llmConfig.USE_CUSTOM_URL || false);
const canChangeKeys = config.can_change_keys;
@ -180,27 +180,12 @@ export default function Home() {
}
const handleSaveConfig = async () => {
if (llmConfig.LLM === 'ollama') {
try {
try {
await handleSaveLLMConfig(llmConfig);
if (llmConfig.LLM === 'ollama') {
setIsLoading(true);
await pullOllamaModels();
toast({
title: 'Success',
description: 'Model downloaded successfully',
});
} catch (error) {
console.error('Error pulling model:', error);
toast({
title: 'Error',
description: 'Failed to download model. Please try again.',
variant: 'destructive',
});
setIsLoading(false);
return;
}
}
try {
await handleSaveLLMConfig(llmConfig, useCustomOllamaUrl);
toast({
title: 'Success',
description: 'Configuration saved successfully',
@ -211,7 +196,7 @@ export default function Home() {
console.error('Error:', error);
toast({
title: 'Error',
description: 'Failed to save configuration',
description: error instanceof Error ? error.message : 'Failed to save configuration',
variant: 'destructive',
});
setIsLoading(false);
@ -225,6 +210,16 @@ export default function Home() {
}
}
const resetDownloadingModel = () => {
setDownloadingModel({
name: '',
size: null,
downloaded: null,
status: '',
done: false,
});
}
const pullOllamaModels = async (): Promise<void> => {
return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
@ -232,21 +227,28 @@ export default function Home() {
const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.MODEL}`);
if (response.status === 200) {
const data = await response.json();
if (data.done) {
if (data.done && data.status !== 'error') {
clearInterval(interval);
setDownloadingModel(data);
resolve();
} else if (data.status === 'error') {
clearInterval(interval);
resetDownloadingModel();
reject(new Error('Error occurred while pulling model'));
} else {
setDownloadingModel(data);
}
} else {
clearInterval(interval);
reject(new Error('Model pulling failed'));
resetDownloadingModel();
if (response.status === 403) {
reject(new Error('Request to Ollama Not Authorized'));
}
reject(new Error('Error occurred while pulling model'));
}
} catch (error) {
console.log('Error fetching ollama models:', error);
clearInterval(interval);
resetDownloadingModel();
reject(error);
}
}, 1000);
@ -263,6 +265,14 @@ export default function Home() {
}
}
const setOllamaConfig = () => {
if (!useCustomOllamaUrl) {
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: 'http://localhost:11434', LLM_API_KEY: undefined, USE_CUSTOM_URL: false });
} else {
setLlmConfig({ ...llmConfig, USE_CUSTOM_URL: true });
}
}
useEffect(() => {
if (!canChangeKeys) {
router.push("/upload");
@ -273,11 +283,7 @@ export default function Home() {
}, []);
useEffect(() => {
if (!useCustomOllamaUrl) {
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: undefined, LLM_API_KEY: undefined });
} else {
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: 'http://localhost:11434', LLM_API_KEY: '' });
}
setOllamaConfig();
}, [useCustomOllamaUrl]);

View file

@ -15,10 +15,13 @@ interface TextFrameProps {
interface LLMConfig {
LLM?: string;
LLM_PROVIDER_URL?: string;
LLM_API_KEY?: string;
OPENAI_API_KEY?: string;
GOOGLE_API_KEY?: string;
PEXELS_API_KEY?: string;
LLM_PROVIDER_URL?: string;
LLM_API_KEY?: string;
MODEL?: string;
// Only used in UI settings
USE_CUSTOM_URL?: boolean;
}

View file

@ -1,8 +1,8 @@
import { setLLMConfig } from "@/store/slices/userConfig";
import { store } from "@/store/store";
export const handleSaveLLMConfig = async (llmConfig: LLMConfig, useCustomOllamaUrl: boolean) => {
if (!hasValidLLMConfig(llmConfig, useCustomOllamaUrl)) {
export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
if (!hasValidLLMConfig(llmConfig)) {
throw new Error('API key cannot be empty');
}
@ -14,21 +14,18 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig, useCustomOllamaU
store.dispatch(setLLMConfig(llmConfig));
}
export const hasValidLLMConfig = (llmConfig: LLMConfig, useCustomOllamaUrl: boolean) => {
export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
if (!llmConfig.LLM) return false;
const OPENAI_API_KEY = llmConfig.OPENAI_API_KEY;
const GOOGLE_API_KEY = llmConfig.GOOGLE_API_KEY;
const MODEL = llmConfig.MODEL;
const PEXELS_API_KEY = llmConfig.PEXELS_API_KEY;
const isOllamaBaseConfigValid = PEXELS_API_KEY !== '' && PEXELS_API_KEY !== null && PEXELS_API_KEY !== undefined && MODEL !== '' && MODEL !== null && MODEL !== undefined;
const isOllamaConfigValid = PEXELS_API_KEY !== '' && PEXELS_API_KEY !== null && PEXELS_API_KEY !== undefined && MODEL !== '' && MODEL !== null && MODEL !== undefined && llmConfig.LLM_PROVIDER_URL !== '' && llmConfig.LLM_PROVIDER_URL !== null && llmConfig.LLM_PROVIDER_URL !== undefined;
return llmConfig.LLM === 'openai' ?
OPENAI_API_KEY !== '' && OPENAI_API_KEY !== null && OPENAI_API_KEY !== undefined :
llmConfig.LLM === 'google' ?
GOOGLE_API_KEY !== '' && GOOGLE_API_KEY !== null && GOOGLE_API_KEY !== undefined :
llmConfig.LLM === 'ollama' ?
useCustomOllamaUrl ?
isOllamaBaseConfigValid && llmConfig.LLM_PROVIDER_URL !== '' && llmConfig.LLM_PROVIDER_URL !== null && llmConfig.LLM_PROVIDER_URL !== undefined && llmConfig.LLM_API_KEY !== '' && llmConfig.LLM_API_KEY !== null && llmConfig.LLM_API_KEY !== undefined :
isOllamaBaseConfigValid : false;
llmConfig.LLM === 'ollama' ? isOllamaConfigValid : false;
}

View file

@ -35,10 +35,13 @@ const setupUserConfigFromEnv = () => {
const userConfig = {
LLM: process.env.LLM || existingConfig.LLM,
LLM_PROVIDER_URL: process.env.LLM_PROVIDER_URL || existingConfig.LLM_PROVIDER_URL,
LLM_API_KEY: process.env.LLM_API_KEY || existingConfig.LLM_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY,
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY,
OLLAMA_MODEL: process.env.OLLAMA_MODEL || existingConfig.OLLAMA_MODEL,
MODEL: process.env.MODEL || existingConfig.MODEL,
PEXELS_API_KEY: process.env.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,
USE_CUSTOM_URL: process.env.USE_CUSTOM_URL || existingConfig.USE_CUSTOM_URL,
};
fs.writeFileSync(userConfigPath, JSON.stringify(userConfig));