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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions backend/src/baserow/api/generative_ai/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class GenerativeAIModelsSerializer(serializers.Serializer):
)


class OpenAISettingsSerializer(GenerativeAIModelsSerializer):
class BaseOpenAISettingsSerializer(GenerativeAIModelsSerializer):
api_key = serializers.CharField(
allow_blank=True,
required=False,
Expand All @@ -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://<your-resource-name>.openai.azure.com,"
"or any other OpenAI compatible API.",
)


class AnthropicSettingsSerializer(GenerativeAIModelsSerializer):
api_key = serializers.CharField(
allow_blank=True,
Expand All @@ -48,7 +58,7 @@ class OllamaSettingsSerializer(GenerativeAIModelsSerializer):
)


class OpenRouterSettingsSerializer(OpenAISettingsSerializer):
class OpenRouterSettingsSerializer(BaseOpenAISettingsSerializer):
api_key = serializers.CharField(
allow_blank=True,
required=False,
Expand Down
1 change: 1 addition & 0 deletions backend/src/baserow/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down
31 changes: 20 additions & 11 deletions backend/src/baserow/core/generative_ai/generative_ai_model_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<FormInput
v-model.trim="v$.settings[type][setting.key].$model"
:error="v$.settings[type][setting.key].$error"
:placeholder="setting.placeholder || null"
/>

<template v-if="setting.description" #helper>
Expand Down Expand Up @@ -199,11 +203,17 @@ export default {
enabled[typeName].length > 0
)
},
getSettingErrorMessage(object) {
if (!object.$error || !object.$errors || object.$errors.length === 0) {
return null
}
return object.$errors.map((error) => error.$message).join(' ')
},
},
validations() {
const settings = this.modelTypes.reduce((acc, [type, modelType]) => {
acc[type] = modelType.getSettings().reduce((acc, setting) => {
acc[setting.key] = {}
acc[setting.key] = setting.validations || {}
return acc
}, {})
return acc
Expand Down
9 changes: 9 additions & 0 deletions web-frontend/modules/core/generativeAIModelTypes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Registerable } from '@baserow/modules/core/registry'
import { url, helpers } from '@vuelidate/validators'

export class GenerativeAIModelType extends Registerable {
get name() {
Expand Down Expand Up @@ -60,6 +61,14 @@ export class OpenAIModelType extends GenerativeAIModelType {
label: i18n.t('generativeAIModelType.openaiOrganization'),
optional: true,
},
{
key: 'base_url',
label: i18n.t('generativeAIModelType.openaiBaseUrl'),
description: i18n.t('generativeAIModelType.openaiBaseUrlDescription'),
validations: {
url: helpers.withMessage(this.app.i18n.t('error.invalidURL'), url),
},
},
modelSettings(
i18n.t('generativeAIModelType.openaiModelsLabel'),
i18n.t('generativeAIModelType.openaiModelsDescription')
Expand Down
4 changes: 3 additions & 1 deletion web-frontend/modules/core/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"close": "Close",
"types": {
"applications": "Applications",
"tables": "Tables",
"tables": "Tables",
"fields": "Fields",
"rows": "Rows"
}
Expand Down Expand Up @@ -308,6 +308,8 @@
"openaiOrganization": "Organization (optional)",
"openaiModelsLabel": "Enabled Models",
"openaiModelsDescription": "Provide a list of comma separated [OpenAI models](https://platform.openai.com/docs/models/continuous-model-upgrades) that can be used in Baserow. (e.g. `gpt-3.5-turbo,gpt-4`)",
"openaiBaseUrl": "Base URL",
"openaiBaseUrlDescription": "Uses the default OpenAI base URL by default if empty. Can optionally be changed to https://eu.api.openai.com/v1, https://<your-resource-name>.openai.azure.com, or any other OpenAI compatible API.",
"anthropic": "Anthropic",
"anthropicApiKeyLabel": "API Key",
"anthropicApiKeyDescription": "Provide an Anthropic API key if you would like to enable the integration. [Instructions on getting an API key](https://docs.anthropic.com/en/api/getting-started).",
Expand Down
Loading