Skip to content

Support a custom base URL for the OpenAI embedding/LLM provider (self-hosted gateway support)#2063

Open
galovics wants to merge 4 commits into
langflow-ai:mainfrom
galovics:feat/openai-custom-base-url
Open

Support a custom base URL for the OpenAI embedding/LLM provider (self-hosted gateway support)#2063
galovics wants to merge 4 commits into
langflow-ai:mainfrom
galovics:feat/openai-custom-base-url

Conversation

@galovics

@galovics galovics commented Jul 9, 2026

Copy link
Copy Markdown

Fixes #2060

Summary

Adds an openai.base_url override (config field, OPENAI_BASE_URL env var, and settings API field) so
OpenRAG's OpenAI provider can be pointed at a self-hosted OpenAI-compatible gateway (e.g. a LiteLLM proxy)
instead of api.openai.com.

  • Threads the override into all three AsyncOpenAI(...) construction sites in patched_async_client
    (src/config/settings.py), including the HTTP/2 capability probe — leaving that one unpatched would
    still make a live call to api.openai.com on every client init even with a custom gateway configured.
  • Extends the Langflow-flow ingest path too: field_mappings["api_base"] in flows_service.py previously
    only mapped ollamaOLLAMA_BASE_URL; this adds openaiOPENAI_API_BASE, and langflow_sync.py
    sets that Langflow global variable when the override is configured.
  • Wires the settings API request/response models and persists the value through both the update_settings
    and onboarding handlers, mirroring the existing watsonx_endpoint pattern — deliberately not marking
    openai.configured = true from a bare base_url, since it's an optional override, not a credential, and
    configured is consumed elsewhere as "this provider is a complete, usable setup."

Why

OpenRAG's OpenAI provider always talks to api.openai.com today. There's no way to route embedding/LLM
traffic through a centrally-managed self-hosted gateway without forking — a common pattern for teams running
their own LiteLLM proxy for unified cost tracking, virtual keys, or reaching providers OpenRAG doesn't
natively support (e.g. Cohere Embed Multilingual served via AWS Bedrock, reached through the gateway).

A correctness note for anyone extending this

OpenRAG's embedding client is patched by agentd.patch.patch_openai_with_mcp. A model name with no
recognized non-OpenAI litellm prefix resolves to provider == "openai" and takes the real OpenAI SDK code
path (an actual HTTP call to whatever base_url is configured), not the in-process litellm SDK path. That
means any non-standard parameter a downstream model needs (e.g. Cohere's input_type when the gateway
fronts Cohere-on-Bedrock) must be passed via the OpenAI SDK's extra_body, not as a bare kwarg, on this
specific code path — verified against the real agentd patch source, not assumed.

Testing

  • Unit tests for the config field (default/round-trip/env-override), the patched_async_client construction
    (asserts the base_url kwarg directly across all three construction sites, with and without the override
    configured), the flows_service.py field-mapping change, and the settings-API write path (persists
    correctly, doesn't mark configured).
  • A real integration test against a locally running LiteLLM proxy + a mock embeddings stub: configures the
    override, builds the real patched_async_client, calls .embeddings.create(..., extra_body={"input_type": "search_document"}), and confirms both a successful response and that the stub actually received
    input_type="search_document" — proving the full chain (config → real OpenAI SDK → real Docker LiteLLM
    proxy → stub) works, including the extra_body passthrough mechanism.
  • Full suite: uv run pytest tests/unit -q → 901 passed, 13 failed. The 13 failures are pre-existing at the
    base commit (unrelated to this change — collection errors from a partially-landed refactor upstream); zero
    new failures.

Out of scope / follow-ups

  • Frontend/Settings-UI wiring for the new field is not included — this PR is backend-only.
  • The current integration test relies on a test-only litellm.register_model(...) call to teach litellm's
    local provider table that an arbitrary gateway route alias should resolve to openai; production code
    does not replicate this. An arbitrary custom-gateway route name that litellm doesn't already recognize as
    OpenAI (real model name, or openai/-prefixed) will still raise BadRequestError in agentd before any
    HTTP call — a pre-existing agentd/litellm constraint, not something this PR changes. Worth a tracked
    follow-up if first-class support for arbitrary route aliases is wanted.

Summary by CodeRabbit

  • New Features
    • Added an optional custom OpenAI-compatible base_url (configured via setup/settings/onboarding) to route requests through OpenAI-compatible gateways.
    • Automatically syncs the OpenAI gateway base URL into flow field mappings and global variables when configured.
  • Bug Fixes
    • Persisted openai_base_url is trimmed and rejects whitespace-only values.
    • Removing OpenAI configuration now also clears the stored OpenAI base_url.
  • Documentation
    • Updated .env.example with OPENAI_BASE_URL.
  • Tests
    • Added unit/integration tests covering env overrides, settings persistence, client base URL behavior, and passthrough through a LiteLLM proxy.

@github-actions github-actions Bot added backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) tests community labels Jul 9, 2026
@galovics

galovics commented Jul 9, 2026

Copy link
Copy Markdown
Author

Companion PRs for the other two Cohere-hosting backends: #2064 (AWS Bedrock) and #2065 (OCI GenAI). All three are independent and can be reviewed/merged in any order, though they touch some of the same files and may need a rebase if merged out of order.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds OpenAI base URL handling across config loading, settings APIs, client construction, Langflow sync, and flow mapping. The PR also adds unit and integration tests covering persistence, validation, client wiring, and proxy request forwarding.

Changes

OpenAI Base URL Override

Layer / File(s) Summary
Config schema and env override
.env.example, src/config/config_manager.py, tests/unit/config/test_openai_base_url_config.py
Adds base_url to OpenAIConfig, reads OPENAI_BASE_URL from the environment, documents the variable, and tests config round-tripping and override behavior.
Settings models and persistence
src/api/settings/models.py, src/api/settings/endpoints.py, tests/unit/test_settings_openai_base_url.py
Adds openai_base_url to settings/onboarding bodies, exposes base_url in settings responses, persists and clears the stored value, and validates blank inputs.
Patched AsyncOpenAI client base_url usage
src/config/settings.py, tests/unit/config/test_patched_async_client_base_url.py, tests/integration/core/test_openai_custom_base_url_litellm_proxy.py
Passes the configured base URL into AsyncOpenAI construction and verifies the client behavior with unit tests plus a LiteLLM proxy integration test.
Langflow ingest path base_url mapping
src/services/flows_service.py, src/api/settings/langflow_sync.py, tests/unit/services/test_flows_service_api_base_mapping.py
Maps openai api_base to OPENAI_API_BASE and syncs that Langflow global variable when the OpenAI base URL is set.
Embedding field normalization test
tests/unit/test_embedding_fields.py
Adds coverage for normalize_model_name handling of closely related Cohere model names.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related issues

Possibly related PRs

  • langflow-ai/openrag#1600: Touches the /settings provider update flow in the same area as the openai_base_url persistence changes.

Suggested labels: enhancement

Suggested reviewers: edwinjosechittilappilly, mfortman11

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The new embedding_fields normalization test for Cohere model variants is unrelated to the OpenAI base URL gateway support objective. Move that regression test to a separate PR unless it is required for this change.
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a custom OpenAI base URL for self-hosted gateway support.
Linked Issues check ✅ Passed The changes add the OpenAI base URL override through config, settings, client wiring, Langflow mapping, and tests, matching the linked issue requirements.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/settings/endpoints.py (1)

660-691: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear current_config.providers.openai.base_url when removing OpenAI.
update_settings only updates openai_base_url when it is explicitly sent, so leaving it here makes a later OpenAI re-enable keep routing through the old gateway instead of the default endpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/settings/endpoints.py` around lines 660 - 691, The OpenAI removal
path in update_settings leaves current_config.providers.openai.base_url behind,
which can preserve an old gateway URL for later re-enables. In the
remove_openai_config branch, alongside clearing api_key and configured on
current_config.providers.openai, also reset base_url to an empty/default state
so the next OpenAI enable uses the standard endpoint; use the existing
update_settings and current_config.providers.openai symbols to place the change
correctly.
🧹 Nitpick comments (1)
tests/unit/config/test_openai_base_url_config.py (1)

133-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unrelated test in a base_url-specific test file.

test_normalize_model_name_still_distinguishes_cohere_variants tests normalize_model_name from utils.embedding_fields, which has no connection to the OpenAI base_url override. Including it in test_openai_base_url_config.py muddies the file's purpose. Consider moving it to a more appropriate test module (e.g., a test_embedding_fields.py or similar).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/config/test_openai_base_url_config.py` around lines 133 - 141,
Move the unrelated normalize_model_name test out of the OpenAI base_url config
test module and into a more appropriate embedding-focused test file. The test
named test_normalize_model_name_still_distinguishes_cohere_variants imports
normalize_model_name from utils.embedding_fields, so relocate it to a dedicated
embedding test module (for example, a test_embedding_fields suite) and keep
test_openai_base_url_config.py focused only on base_url behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/settings/models.py`:
- Line 29: The openai_base_url setting currently allows whitespace-only values
because Field(None, min_length=1) does not trim input, so update the validation
in the Settings model to strip leading/trailing whitespace before enforcing
non-empty input. Apply the fix in the openai_base_url field handling in
Settings, and make sure update_settings and onboarding still persist the
normalized value consistently so whitespace-only input is rejected instead of
becoming an empty override.

---

Outside diff comments:
In `@src/api/settings/endpoints.py`:
- Around line 660-691: The OpenAI removal path in update_settings leaves
current_config.providers.openai.base_url behind, which can preserve an old
gateway URL for later re-enables. In the remove_openai_config branch, alongside
clearing api_key and configured on current_config.providers.openai, also reset
base_url to an empty/default state so the next OpenAI enable uses the standard
endpoint; use the existing update_settings and current_config.providers.openai
symbols to place the change correctly.

---

Nitpick comments:
In `@tests/unit/config/test_openai_base_url_config.py`:
- Around line 133-141: Move the unrelated normalize_model_name test out of the
OpenAI base_url config test module and into a more appropriate embedding-focused
test file. The test named
test_normalize_model_name_still_distinguishes_cohere_variants imports
normalize_model_name from utils.embedding_fields, so relocate it to a dedicated
embedding test module (for example, a test_embedding_fields suite) and keep
test_openai_base_url_config.py focused only on base_url behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd1842f5-8744-44ad-9cea-78d43a08fb7d

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0f30b and a818b30.

📒 Files selected for processing (12)
  • .env.example
  • src/api/settings/endpoints.py
  • src/api/settings/langflow_sync.py
  • src/api/settings/models.py
  • src/config/config_manager.py
  • src/config/settings.py
  • src/services/flows_service.py
  • tests/integration/core/test_openai_custom_base_url_litellm_proxy.py
  • tests/unit/config/test_openai_base_url_config.py
  • tests/unit/config/test_patched_async_client_base_url.py
  • tests/unit/services/test_flows_service_api_base_mapping.py
  • tests/unit/test_settings_openai_base_url.py

Comment thread src/api/settings/models.py
galovics added a commit to galovics/openrag that referenced this pull request Jul 9, 2026
- Clear providers.openai.base_url (not just api_key/configured) when
  removing the OpenAI config, so a later re-enable falls back to
  api.openai.com instead of silently reusing a stale gateway URL.
- Reject whitespace-only openai_base_url in SettingsUpdateBody and
  OnboardingBody via a field_validator, instead of silently accepting
  it and stripping it down to an empty (no-op) value downstream.
- Move test_normalize_model_name_still_distinguishes_cohere_variants
  out of the base_url-focused config test file into
  test_embedding_fields.py, where the rest of that module's coverage
  lives.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/unit/test_settings_openai_base_url.py (1)

141-173: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing _patch_update_settings_deps in the onboarding test.

The onboarding test duplicates the monkeypatch setup (lines 147-156) instead of calling _patch_update_settings_deps. If the only reason is the additional wait_for_langflow stub, you could extend the helper with an optional flag or add the wait_for_langflow patch after calling it, reducing duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_settings_openai_base_url.py` around lines 141 - 173, The
onboarding test duplicates the dependency monkeypatch setup already covered by
_patch_update_settings_deps; refactor test_onboarding_persists_openai_base_url
to call that helper for the shared patches and then add only the extra
wait_for_langflow stub needed for onboarding. If the helper lacks this hook,
extend _patch_update_settings_deps with an optional flag or allow the caller to
patch wait_for_langflow afterward, keeping the onboarding test focused on its
unique behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/test_settings_openai_base_url.py`:
- Around line 141-173: The onboarding test duplicates the dependency monkeypatch
setup already covered by _patch_update_settings_deps; refactor
test_onboarding_persists_openai_base_url to call that helper for the shared
patches and then add only the extra wait_for_langflow stub needed for
onboarding. If the helper lacks this hook, extend _patch_update_settings_deps
with an optional flag or allow the caller to patch wait_for_langflow afterward,
keeping the onboarding test focused on its unique behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 249faa4f-51c4-444e-87fe-d06983d0203c

📥 Commits

Reviewing files that changed from the base of the PR and between a818b30 and ea1b2fd.

📒 Files selected for processing (5)
  • src/api/settings/endpoints.py
  • src/api/settings/models.py
  • tests/unit/config/test_openai_base_url_config.py
  • tests/unit/test_embedding_fields.py
  • tests/unit/test_settings_openai_base_url.py
💤 Files with no reviewable changes (1)
  • tests/unit/config/test_openai_base_url_config.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/api/settings/endpoints.py
  • src/api/settings/models.py

galovics added 4 commits July 9, 2026 20:38
…flow-ai#2060)

Adds an openai.base_url override (config, env var OPENAI_BASE_URL, and
settings API field) so OpenRAG's OpenAI provider can be pointed at a
self-hosted OpenAI-compatible gateway (e.g. a LiteLLM proxy) instead of
api.openai.com. Threads the override into all three AsyncOpenAI(...)
construction sites in patched_async_client (including the HTTP/2 probe,
which would otherwise still probe api.openai.com), and into the
Langflow-flow ingest path via the existing api_base field-mapping /
OPENAI_API_BASE global variable pattern already used for Ollama.

Verified end to end against a local LiteLLM proxy + mock embeddings
stub: the configured base_url reaches the real AsyncOpenAI client, and
extra_body input_type passthrough works through the full chain.
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.
- Clear providers.openai.base_url (not just api_key/configured) when
  removing the OpenAI config, so a later re-enable falls back to
  api.openai.com instead of silently reusing a stale gateway URL.
- Reject whitespace-only openai_base_url in SettingsUpdateBody and
  OnboardingBody via a field_validator, instead of silently accepting
  it and stripping it down to an empty (no-op) value downstream.
- Move test_normalize_model_name_still_distinguishes_cohere_variants
  out of the base_url-focused config test file into
  test_embedding_fields.py, where the rest of that module's coverage
  lives.
Upstream's atomicity fix (4d1be69) made update_settings() stage all
mutations on a deep-copied working_config, only saving it at the end,
instead of mutating the live current_config in place. The openai_base_url
regression tests asserted on the original config object, which is no
longer mutated - capture the object passed to save_config_file instead,
matching the pattern used in test_settings_index_name_validation.py.
@galovics
galovics force-pushed the feat/openai-custom-base-url branch from ea1b2fd to 5e20829 Compare July 9, 2026 18:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/integration/core/test_openai_custom_base_url_litellm_proxy.py (1)

139-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clean up litellm.register_model global state after the test.

litellm.register_model mutates litellm's global model registry and is never reverted. If other tests in the same session use litellm with the same or overlapping model names, this could affect their provider classification. Consider snapshotting and restoring the registration in a fixture.

♻️ Suggested cleanup fixture
+@pytest.fixture(autouse=True)
+def _restore_litellm_model_registry():
+    original = dict(litellm.model_cost)
+    yield
+    litellm.model_cost.clear()
+    litellm.model_cost.update(original)
+
+
 `@pytest.mark.skipif`(
     not _proxy_is_reachable(),
     reason=(
         "Local LiteLLM proxy not reachable at "
         f"{LITELLM_PROXY_URL} - skipping the real-proxy integration test."
     ),
 )
 async def test_custom_base_url_reaches_real_litellm_proxy_with_input_type_passthrough():
     # See module docstring: teaches litellm's local provider-detection table
     # that this proxy route alias belongs to "openai" so agentd's patched
     # client forwards the call to the real OpenAI SDK against our base_url
     # instead of raising BadRequestError before any network call happens.
     litellm.register_model(
         {EMBEDDING_MODEL: {"litellm_provider": "openai", "mode": "embedding"}}
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/core/test_openai_custom_base_url_litellm_proxy.py` around
lines 139 - 141, The test mutates litellm’s global model registry via
register_model and leaves that state behind. Update the test setup around the
EMBEDDING_MODEL registration to snapshot the existing registry state before
calling litellm.register_model, then restore it after the test (preferably with
a fixture or teardown) so other tests are not affected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/integration/core/test_openai_custom_base_url_litellm_proxy.py`:
- Around line 139-141: The test mutates litellm’s global model registry via
register_model and leaves that state behind. Update the test setup around the
EMBEDDING_MODEL registration to snapshot the existing registry state before
calling litellm.register_model, then restore it after the test (preferably with
a fixture or teardown) so other tests are not affected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28e1e24f-4a1f-4d58-8107-b38ceaf3869f

📥 Commits

Reviewing files that changed from the base of the PR and between ea1b2fd and 5e20829.

📒 Files selected for processing (13)
  • .env.example
  • src/api/settings/endpoints.py
  • src/api/settings/langflow_sync.py
  • src/api/settings/models.py
  • src/config/config_manager.py
  • src/config/settings.py
  • src/services/flows_service.py
  • tests/integration/core/test_openai_custom_base_url_litellm_proxy.py
  • tests/unit/config/test_openai_base_url_config.py
  • tests/unit/config/test_patched_async_client_base_url.py
  • tests/unit/services/test_flows_service_api_base_mapping.py
  • tests/unit/test_embedding_fields.py
  • tests/unit/test_settings_openai_base_url.py
✅ Files skipped from review due to trivial changes (2)
  • src/api/settings/langflow_sync.py
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/unit/services/test_flows_service_api_base_mapping.py
  • src/config/config_manager.py
  • tests/unit/test_embedding_fields.py
  • src/api/settings/models.py
  • src/config/settings.py
  • src/services/flows_service.py
  • tests/unit/test_settings_openai_base_url.py
  • src/api/settings/endpoints.py
  • tests/unit/config/test_patched_async_client_base_url.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) community tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support a custom base URL for the OpenAI embedding/LLM provider (self-hosted gateway support)

1 participant