From 70be921ea88c042e388757d3fea6c6d1c2bf03ef Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 7 Nov 2025 16:06:25 +0100 Subject: [PATCH] Allow setting custom OpenAI base url (#4137) --- .../baserow/api/generative_ai/serializers.py | 14 +++++++-- backend/src/baserow/config/settings/base.py | 1 + .../generative_ai_model_types.py | 31 ++++++++++++------- .../feature/4108_openai_custom_base_url.json | 9 ++++++ docker-compose.yml | 1 + .../GenerativeAIWorkspaceSettings.vue | 12 ++++++- .../modules/core/generativeAIModelTypes.js | 9 ++++++ web-frontend/modules/core/locales/en.json | 4 ++- 8 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 changelog/entries/unreleased/feature/4108_openai_custom_base_url.json diff --git a/backend/src/baserow/api/generative_ai/serializers.py b/backend/src/baserow/api/generative_ai/serializers.py index 0ff2f9aedf..084767f84b 100644 --- a/backend/src/baserow/api/generative_ai/serializers.py +++ b/backend/src/baserow/api/generative_ai/serializers.py @@ -9,7 +9,7 @@ class GenerativeAIModelsSerializer(serializers.Serializer): ) -class OpenAISettingsSerializer(GenerativeAIModelsSerializer): +class BaseOpenAISettingsSerializer(GenerativeAIModelsSerializer): api_key = serializers.CharField( allow_blank=True, required=False, @@ -22,6 +22,16 @@ class OpenAISettingsSerializer(GenerativeAIModelsSerializer): ) +class OpenAISettingsSerializer(BaseOpenAISettingsSerializer): + base_url = serializers.URLField( + allow_blank=True, + required=False, + help_text="https://api.openai.com/v1 by default, but can be changed to " + "https://eu.api.openai.com/v1, https://.openai.azure.com," + "or any other OpenAI compatible API.", + ) + + class AnthropicSettingsSerializer(GenerativeAIModelsSerializer): api_key = serializers.CharField( allow_blank=True, @@ -48,7 +58,7 @@ class OllamaSettingsSerializer(GenerativeAIModelsSerializer): ) -class OpenRouterSettingsSerializer(OpenAISettingsSerializer): +class OpenRouterSettingsSerializer(BaseOpenAISettingsSerializer): api_key = serializers.CharField( allow_blank=True, required=False, diff --git a/backend/src/baserow/config/settings/base.py b/backend/src/baserow/config/settings/base.py index 70d7624ff2..5efa0f0fb1 100644 --- a/backend/src/baserow/config/settings/base.py +++ b/backend/src/baserow/config/settings/base.py @@ -1285,6 +1285,7 @@ def __setitem__(self, key, value): BASEROW_OPENAI_API_KEY = os.getenv("BASEROW_OPENAI_API_KEY", None) BASEROW_OPENAI_ORGANIZATION = os.getenv("BASEROW_OPENAI_ORGANIZATION", "") or None +BASEROW_OPENAI_BASE_URL = os.getenv("BASEROW_OPENAI_BASE_URL", None) or None BASEROW_OPENAI_MODELS = os.getenv("BASEROW_OPENAI_MODELS", "") BASEROW_OPENAI_MODELS = ( BASEROW_OPENAI_MODELS.split(",") if BASEROW_OPENAI_MODELS else [] diff --git a/backend/src/baserow/core/generative_ai/generative_ai_model_types.py b/backend/src/baserow/core/generative_ai/generative_ai_model_types.py index e5ec166eae..9c59cd77da 100644 --- a/backend/src/baserow/core/generative_ai/generative_ai_model_types.py +++ b/backend/src/baserow/core/generative_ai/generative_ai_model_types.py @@ -37,6 +37,9 @@ def get_organization(self, workspace=None, settings_override=None): or settings.BASEROW_OPENAI_ORGANIZATION ) + def get_base_url(self, workspace=None, settings_override=None): + return None + def is_enabled(self, workspace=None, settings_override=None): api_key = self.get_api_key(workspace, settings_override) return bool(api_key) and bool( @@ -48,12 +51,13 @@ def is_enabled(self, workspace=None, settings_override=None): def get_client(self, workspace=None, settings_override=None): api_key = self.get_api_key(workspace, settings_override) organization = self.get_organization(workspace, settings_override) - return OpenAI(api_key=api_key, organization=organization) + base_url = self.get_base_url(workspace, settings_override) + return OpenAI(api_key=api_key, organization=organization, base_url=base_url) def get_settings_serializer(self): - from baserow.api.generative_ai.serializers import OpenAISettingsSerializer + from baserow.api.generative_ai.serializers import BaseOpenAISettingsSerializer - return OpenAISettingsSerializer + return BaseOpenAISettingsSerializer def prompt( self, model, prompt, workspace=None, temperature=None, settings_override=None @@ -79,6 +83,17 @@ class OpenAIGenerativeAIModelType( ): type = "openai" + def get_settings_serializer(self): + from baserow.api.generative_ai.serializers import OpenAISettingsSerializer + + return OpenAISettingsSerializer + + def get_base_url(self, workspace=None, settings_override=None): + return ( + self.get_workspace_setting(workspace, "base_url", settings_override) + or settings.BASEROW_OPENAI_BASE_URL + ) + def is_file_compatible(self, file_name: str) -> bool: # See supported files at: # https://platform.openai.com/docs/assistants/tools/file-search/supported-files @@ -377,14 +392,8 @@ def get_organization(self, workspace=None, settings_override=None): or settings.BASEROW_OPENROUTER_ORGANIZATION ) - def get_client(self, workspace=None, settings_override=None): - api_key = self.get_api_key(workspace, settings_override) - organization = self.get_organization(workspace, settings_override) - return OpenAI( - api_key=api_key, - organization=organization, - base_url="https://openrouter.ai/api/v1", - ) + def get_base_url(self, workspace=None, settings_override=None): + return "https://openrouter.ai/api/v1" def get_settings_serializer(self): from baserow.api.generative_ai.serializers import OpenRouterSettingsSerializer diff --git a/changelog/entries/unreleased/feature/4108_openai_custom_base_url.json b/changelog/entries/unreleased/feature/4108_openai_custom_base_url.json new file mode 100644 index 0000000000..4ebe0c0865 --- /dev/null +++ b/changelog/entries/unreleased/feature/4108_openai_custom_base_url.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Allow setting custom base URL for OpenAI.", + "domain": "core", + "issue_number": 4108, + "issue_origin": "github", + "bullet_points": [], + "created_at": "2025-10-29" +} diff --git a/docker-compose.yml b/docker-compose.yml index 4c59b0b99b..f8e122c19f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -200,6 +200,7 @@ x-backend-variables: BASEROW_OPENAI_API_KEY: BASEROW_OPENAI_ORGANIZATION: BASEROW_OPENAI_MODELS: + BASEROW_OPENAI_BASE_URL: BASEROW_OPENROUTER_API_KEY: BASEROW_OPENROUTER_ORGANIZATION: BASEROW_OPENROUTER_MODELS: diff --git a/web-frontend/modules/core/components/workspace/GenerativeAIWorkspaceSettings.vue b/web-frontend/modules/core/components/workspace/GenerativeAIWorkspaceSettings.vue index b4de586350..655a227e68 100644 --- a/web-frontend/modules/core/components/workspace/GenerativeAIWorkspaceSettings.vue +++ b/web-frontend/modules/core/components/workspace/GenerativeAIWorkspaceSettings.vue @@ -57,12 +57,16 @@ small-label :label="setting.label" :error="v$.settings[type][setting.key].$error" + :error-message=" + getSettingErrorMessage(v$.settings[type][setting.key]) + " required class="margin-bottom-2" >