Skip to content

Commit 70be921

Browse files
authored
Allow setting custom OpenAI base url (baserow#4137)
1 parent 2a3fdc9 commit 70be921

File tree

8 files changed

+66
-15
lines changed

8 files changed

+66
-15
lines changed

backend/src/baserow/api/generative_ai/serializers.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class GenerativeAIModelsSerializer(serializers.Serializer):
99
)
1010

1111

12-
class OpenAISettingsSerializer(GenerativeAIModelsSerializer):
12+
class BaseOpenAISettingsSerializer(GenerativeAIModelsSerializer):
1313
api_key = serializers.CharField(
1414
allow_blank=True,
1515
required=False,
@@ -22,6 +22,16 @@ class OpenAISettingsSerializer(GenerativeAIModelsSerializer):
2222
)
2323

2424

25+
class OpenAISettingsSerializer(BaseOpenAISettingsSerializer):
26+
base_url = serializers.URLField(
27+
allow_blank=True,
28+
required=False,
29+
help_text="https://api.openai.com/v1 by default, but can be changed to "
30+
"https://eu.api.openai.com/v1, https://<your-resource-name>.openai.azure.com,"
31+
"or any other OpenAI compatible API.",
32+
)
33+
34+
2535
class AnthropicSettingsSerializer(GenerativeAIModelsSerializer):
2636
api_key = serializers.CharField(
2737
allow_blank=True,
@@ -48,7 +58,7 @@ class OllamaSettingsSerializer(GenerativeAIModelsSerializer):
4858
)
4959

5060

51-
class OpenRouterSettingsSerializer(OpenAISettingsSerializer):
61+
class OpenRouterSettingsSerializer(BaseOpenAISettingsSerializer):
5262
api_key = serializers.CharField(
5363
allow_blank=True,
5464
required=False,

backend/src/baserow/config/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,7 @@ def __setitem__(self, key, value):
12851285

12861286
BASEROW_OPENAI_API_KEY = os.getenv("BASEROW_OPENAI_API_KEY", None)
12871287
BASEROW_OPENAI_ORGANIZATION = os.getenv("BASEROW_OPENAI_ORGANIZATION", "") or None
1288+
BASEROW_OPENAI_BASE_URL = os.getenv("BASEROW_OPENAI_BASE_URL", None) or None
12881289
BASEROW_OPENAI_MODELS = os.getenv("BASEROW_OPENAI_MODELS", "")
12891290
BASEROW_OPENAI_MODELS = (
12901291
BASEROW_OPENAI_MODELS.split(",") if BASEROW_OPENAI_MODELS else []

backend/src/baserow/core/generative_ai/generative_ai_model_types.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def get_organization(self, workspace=None, settings_override=None):
3737
or settings.BASEROW_OPENAI_ORGANIZATION
3838
)
3939

40+
def get_base_url(self, workspace=None, settings_override=None):
41+
return None
42+
4043
def is_enabled(self, workspace=None, settings_override=None):
4144
api_key = self.get_api_key(workspace, settings_override)
4245
return bool(api_key) and bool(
@@ -48,12 +51,13 @@ def is_enabled(self, workspace=None, settings_override=None):
4851
def get_client(self, workspace=None, settings_override=None):
4952
api_key = self.get_api_key(workspace, settings_override)
5053
organization = self.get_organization(workspace, settings_override)
51-
return OpenAI(api_key=api_key, organization=organization)
54+
base_url = self.get_base_url(workspace, settings_override)
55+
return OpenAI(api_key=api_key, organization=organization, base_url=base_url)
5256

5357
def get_settings_serializer(self):
54-
from baserow.api.generative_ai.serializers import OpenAISettingsSerializer
58+
from baserow.api.generative_ai.serializers import BaseOpenAISettingsSerializer
5559

56-
return OpenAISettingsSerializer
60+
return BaseOpenAISettingsSerializer
5761

5862
def prompt(
5963
self, model, prompt, workspace=None, temperature=None, settings_override=None
@@ -79,6 +83,17 @@ class OpenAIGenerativeAIModelType(
7983
):
8084
type = "openai"
8185

86+
def get_settings_serializer(self):
87+
from baserow.api.generative_ai.serializers import OpenAISettingsSerializer
88+
89+
return OpenAISettingsSerializer
90+
91+
def get_base_url(self, workspace=None, settings_override=None):
92+
return (
93+
self.get_workspace_setting(workspace, "base_url", settings_override)
94+
or settings.BASEROW_OPENAI_BASE_URL
95+
)
96+
8297
def is_file_compatible(self, file_name: str) -> bool:
8398
# See supported files at:
8499
# https://platform.openai.com/docs/assistants/tools/file-search/supported-files
@@ -377,14 +392,8 @@ def get_organization(self, workspace=None, settings_override=None):
377392
or settings.BASEROW_OPENROUTER_ORGANIZATION
378393
)
379394

380-
def get_client(self, workspace=None, settings_override=None):
381-
api_key = self.get_api_key(workspace, settings_override)
382-
organization = self.get_organization(workspace, settings_override)
383-
return OpenAI(
384-
api_key=api_key,
385-
organization=organization,
386-
base_url="https://openrouter.ai/api/v1",
387-
)
395+
def get_base_url(self, workspace=None, settings_override=None):
396+
return "https://openrouter.ai/api/v1"
388397

389398
def get_settings_serializer(self):
390399
from baserow.api.generative_ai.serializers import OpenRouterSettingsSerializer
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Allow setting custom base URL for OpenAI.",
4+
"domain": "core",
5+
"issue_number": 4108,
6+
"issue_origin": "github",
7+
"bullet_points": [],
8+
"created_at": "2025-10-29"
9+
}

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ x-backend-variables:
200200
BASEROW_OPENAI_API_KEY:
201201
BASEROW_OPENAI_ORGANIZATION:
202202
BASEROW_OPENAI_MODELS:
203+
BASEROW_OPENAI_BASE_URL:
203204
BASEROW_OPENROUTER_API_KEY:
204205
BASEROW_OPENROUTER_ORGANIZATION:
205206
BASEROW_OPENROUTER_MODELS:

web-frontend/modules/core/components/workspace/GenerativeAIWorkspaceSettings.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,16 @@
5757
small-label
5858
:label="setting.label"
5959
:error="v$.settings[type][setting.key].$error"
60+
:error-message="
61+
getSettingErrorMessage(v$.settings[type][setting.key])
62+
"
6063
required
6164
class="margin-bottom-2"
6265
>
6366
<FormInput
6467
v-model.trim="v$.settings[type][setting.key].$model"
6568
:error="v$.settings[type][setting.key].$error"
69+
:placeholder="setting.placeholder || null"
6670
/>
6771

6872
<template v-if="setting.description" #helper>
@@ -199,11 +203,17 @@ export default {
199203
enabled[typeName].length > 0
200204
)
201205
},
206+
getSettingErrorMessage(object) {
207+
if (!object.$error || !object.$errors || object.$errors.length === 0) {
208+
return null
209+
}
210+
return object.$errors.map((error) => error.$message).join(' ')
211+
},
202212
},
203213
validations() {
204214
const settings = this.modelTypes.reduce((acc, [type, modelType]) => {
205215
acc[type] = modelType.getSettings().reduce((acc, setting) => {
206-
acc[setting.key] = {}
216+
acc[setting.key] = setting.validations || {}
207217
return acc
208218
}, {})
209219
return acc

web-frontend/modules/core/generativeAIModelTypes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Registerable } from '@baserow/modules/core/registry'
2+
import { url, helpers } from '@vuelidate/validators'
23

34
export class GenerativeAIModelType extends Registerable {
45
get name() {
@@ -60,6 +61,14 @@ export class OpenAIModelType extends GenerativeAIModelType {
6061
label: i18n.t('generativeAIModelType.openaiOrganization'),
6162
optional: true,
6263
},
64+
{
65+
key: 'base_url',
66+
label: i18n.t('generativeAIModelType.openaiBaseUrl'),
67+
description: i18n.t('generativeAIModelType.openaiBaseUrlDescription'),
68+
validations: {
69+
url: helpers.withMessage(this.app.i18n.t('error.invalidURL'), url),
70+
},
71+
},
6372
modelSettings(
6473
i18n.t('generativeAIModelType.openaiModelsLabel'),
6574
i18n.t('generativeAIModelType.openaiModelsDescription')

web-frontend/modules/core/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"close": "Close",
1717
"types": {
1818
"applications": "Applications",
19-
"tables": "Tables",
19+
"tables": "Tables",
2020
"fields": "Fields",
2121
"rows": "Rows"
2222
}
@@ -308,6 +308,8 @@
308308
"openaiOrganization": "Organization (optional)",
309309
"openaiModelsLabel": "Enabled Models",
310310
"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`)",
311+
"openaiBaseUrl": "Base URL",
312+
"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.",
311313
"anthropic": "Anthropic",
312314
"anthropicApiKeyLabel": "API Key",
313315
"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).",

0 commit comments

Comments
 (0)