Skip to content

Commit a818b30

Browse files
committed
fix: persist openai_base_url through the settings write path
SettingsUpdateBody.openai_base_url and OnboardingBody.openai_base_url were accepted by the settings/onboarding APIs but never applied to current_config.providers.openai.base_url, so a client POSTing openai_base_url got a silent no-op. Wire it up the same way watsonx_endpoint is applied in both handlers, but without marking OpenAI as "configured" on its own since base_url is an optional override, not a credential. Also add openai_base_url to the provider_fields list gating the providers:write RBAC check, matching the other non-secret endpoint fields (watsonx_endpoint, ollama_endpoint) already there.
1 parent 227ea15 commit a818b30

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/api/settings/endpoints.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ async def update_settings(
278278
"llm_model",
279279
"embedding_model",
280280
"openai_api_key",
281+
"openai_base_url",
281282
"anthropic_api_key",
282283
"watsonx_api_key",
283284
"watsonx_endpoint",
@@ -587,6 +588,13 @@ async def update_settings(
587588
config_updated = True
588589
provider_updated = True
589590

591+
if body.openai_base_url is not None:
592+
# Optional override to point at an OpenAI-compatible gateway. Unlike
593+
# an API key, a base URL alone doesn't mean OpenAI is "configured".
594+
current_config.providers.openai.base_url = body.openai_base_url.strip()
595+
config_updated = True
596+
provider_updated = True
597+
590598
if body.anthropic_api_key is not None and body.anthropic_api_key.strip():
591599
current_config.providers.anthropic.api_key = body.anthropic_api_key
592600
current_config.providers.anthropic.configured = True
@@ -883,6 +891,12 @@ async def onboarding(
883891
current_config.providers.openai.configured = True
884892
config_updated = True
885893

894+
if body.openai_base_url:
895+
# Optional override to point at an OpenAI-compatible gateway. Unlike
896+
# an API key, a base URL alone doesn't mean OpenAI is "configured".
897+
current_config.providers.openai.base_url = body.openai_base_url.strip()
898+
config_updated = True
899+
886900
if body.anthropic_api_key:
887901
current_config.providers.anthropic.api_key = body.anthropic_api_key.strip()
888902
current_config.providers.anthropic.configured = True
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for the OpenAI custom base URL settings write path.
2+
3+
Regression coverage: SettingsUpdateBody.openai_base_url and
4+
OnboardingBody.openai_base_url were accepted by the API but never written
5+
onto current_config.providers.openai.base_url — a client POSTing
6+
openai_base_url got a silent no-op. See src/api/settings/endpoints.py.
7+
"""
8+
9+
from unittest.mock import AsyncMock
10+
11+
import pytest
12+
13+
import api.settings.endpoints as settings_api
14+
from config.config_manager import (
15+
AgentConfig,
16+
AnthropicConfig,
17+
KnowledgeConfig,
18+
OllamaConfig,
19+
OnboardingState,
20+
OpenAIConfig,
21+
OpenRAGConfig,
22+
ProvidersConfig,
23+
WatsonXConfig,
24+
)
25+
26+
27+
class _FakeTask:
28+
def __init__(self):
29+
self.done_callback = None
30+
31+
def add_done_callback(self, cb):
32+
self.done_callback = cb
33+
34+
35+
def _make_config(*, edited: bool = True) -> OpenRAGConfig:
36+
return OpenRAGConfig(
37+
providers=ProvidersConfig(
38+
openai=OpenAIConfig(),
39+
anthropic=AnthropicConfig(),
40+
watsonx=WatsonXConfig(),
41+
ollama=OllamaConfig(),
42+
),
43+
knowledge=KnowledgeConfig(),
44+
agent=AgentConfig(),
45+
onboarding=OnboardingState(),
46+
edited=edited,
47+
)
48+
49+
50+
def _patch_update_settings_deps(monkeypatch, config):
51+
"""Mirrors the mocking pattern used in test_settings_async_post_save.py."""
52+
fake_task = _FakeTask()
53+
54+
async def _noop_refresh():
55+
return None
56+
57+
def _fake_create_task(coro):
58+
# We only care about the config write in these tests, not the
59+
# background Langflow sync that provider changes normally schedule.
60+
coro.close()
61+
return fake_task
62+
63+
monkeypatch.setattr(settings_api, "get_openrag_config", lambda: config, raising=True)
64+
monkeypatch.setattr(
65+
settings_api.config_manager,
66+
"save_config_file",
67+
lambda updated_config: True,
68+
raising=True,
69+
)
70+
monkeypatch.setattr(
71+
settings_api.clients, "refresh_patched_client", _noop_refresh, raising=True
72+
)
73+
monkeypatch.setattr(settings_api.TelemetryClient, "send_event", AsyncMock(), raising=True)
74+
monkeypatch.setattr(settings_api.asyncio, "create_task", _fake_create_task, raising=True)
75+
return fake_task
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_update_settings_persists_openai_base_url(monkeypatch):
80+
settings_api._background_tasks.clear()
81+
config = _make_config()
82+
_patch_update_settings_deps(monkeypatch, config)
83+
84+
response = await settings_api.update_settings(
85+
settings_api.SettingsUpdateBody(openai_base_url="https://gateway.example.com/v1"),
86+
session_manager=object(),
87+
user=None,
88+
)
89+
90+
assert isinstance(response, settings_api.SettingsUpdateResponse)
91+
assert config.providers.openai.base_url == "https://gateway.example.com/v1"
92+
# An optional base_url override should not, by itself, mark OpenAI as
93+
# "configured" the way an api_key does.
94+
assert config.providers.openai.configured is False
95+
96+
97+
@pytest.mark.asyncio
98+
async def test_update_settings_strips_openai_base_url_whitespace(monkeypatch):
99+
settings_api._background_tasks.clear()
100+
config = _make_config()
101+
_patch_update_settings_deps(monkeypatch, config)
102+
103+
await settings_api.update_settings(
104+
settings_api.SettingsUpdateBody(openai_base_url=" https://gateway.example.com/v1 "),
105+
session_manager=object(),
106+
user=None,
107+
)
108+
109+
assert config.providers.openai.base_url == "https://gateway.example.com/v1"
110+
111+
112+
@pytest.mark.asyncio
113+
async def test_onboarding_persists_openai_base_url(monkeypatch):
114+
config = _make_config(edited=False)
115+
116+
async def _noop_refresh():
117+
return None
118+
119+
monkeypatch.setattr(settings_api, "get_openrag_config", lambda: config, raising=True)
120+
monkeypatch.setattr(
121+
settings_api.config_manager,
122+
"save_config_file",
123+
lambda updated_config: True,
124+
raising=True,
125+
)
126+
monkeypatch.setattr(
127+
settings_api.clients, "refresh_patched_client", _noop_refresh, raising=True
128+
)
129+
monkeypatch.setattr(settings_api.TelemetryClient, "send_event", AsyncMock(), raising=True)
130+
monkeypatch.setattr(settings_api, "wait_for_langflow", AsyncMock(), raising=True)
131+
132+
response = await settings_api.onboarding(
133+
settings_api.OnboardingBody(openai_base_url="https://gateway.example.com/v1"),
134+
flows_service=None,
135+
session_manager=object(),
136+
document_service=None,
137+
models_service=None,
138+
task_service=None,
139+
langflow_file_service=None,
140+
knowledge_filter_service=None,
141+
user=None,
142+
)
143+
144+
assert isinstance(response, settings_api.OnboardingResponse)
145+
assert config.providers.openai.base_url == "https://gateway.example.com/v1"
146+
assert config.providers.openai.configured is False

0 commit comments

Comments
 (0)