Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8fa0cc5
support a custom openai api base
matanor Jan 20, 2026
90f8b04
Merge remote-tracking branch 'remotes/upstream/main' into custom_open…
matanor Jan 29, 2026
cc2187b
fix merge
matanor Jan 29, 2026
1794b0d
further changes to support custom endpoints
matanor Jan 29, 2026
16a1ad4
add missing field
matanor Jan 29, 2026
c72358e
fix env var name
matanor Jan 29, 2026
e200f6b
openai providers startwith 'openai'
matanor Feb 1, 2026
830271f
revert to a single openai provider
matanor Feb 1, 2026
fe55622
revert to a single openai provider
matanor Feb 1, 2026
7f6ebe6
revert to a single openai provider
matanor Feb 1, 2026
787e5da
fix LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT
matanor Feb 1, 2026
371029f
throw if api_base is not in request or in configuration (as done for …
matanor Feb 1, 2026
6ccffb8
Merge branch 'main' into custom_openai_api_base
matanor Feb 8, 2026
4241f31
add OPENAI_API_BASE to add_provider_credentials_to_headers() and buil…
matanor Feb 8, 2026
6d2e809
add OPENAI_API_BASE as default for the api_base field of the OPENAI e…
matanor Feb 9, 2026
73ab1b3
add OPENAI_API_BASE to embedder and agent
matanor Feb 9, 2026
6624c4c
when updating an openai api_base, use the OPENAI_API_BASE
matanor Feb 9, 2026
9fbb7cc
add claude-opus to OpenAI allowed LLMs
matanor Feb 9, 2026
4a1a39a
update openai_api_base when updating the template
matanor Feb 10, 2026
a1beeb2
fix for redundant /v1 extension
matanor Feb 23, 2026
b49315f
update OPEN_API_BASE env variable when the endpoint is loaded from th…
matanor Mar 9, 2026
66a595b
Merge remote-tracking branch 'remotes/origin/main' into custom_openai…
matanor Mar 9, 2026
8fc4ce3
update api_base setup in get_openai_models
matanor Mar 9, 2026
0a16084
remove default of None
matanor Mar 11, 2026
d3a4543
revert flow changes back to main
matanor Mar 12, 2026
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
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ services:
- OPENSEARCH_USERNAME=${OPENSEARCH_USERNAME:-admin}
- OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- OPENAI_API_BASE=${OPENAI_API_BASE}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- WATSONX_API_KEY=${WATSONX_API_KEY}
- WATSONX_ENDPOINT=${WATSONX_ENDPOINT}
Expand Down Expand Up @@ -132,6 +133,7 @@ services:
- LANGFUSE_HOST=${LANGFUSE_HOST:-}
- LANGFLOW_DEACTIVATE_TRACING
- OPENAI_API_KEY=${OPENAI_API_KEY:-None}
- OPENAI_API_BASE=${OPENAI_API_BASE:-None}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-None}
- WATSONX_API_KEY=${WATSONX_API_KEY:-None}
- WATSONX_ENDPOINT=${WATSONX_ENDPOINT:-None}
Expand Down Expand Up @@ -160,7 +162,7 @@ services:
- MIMETYPE=None
- FILESIZE=0
- SELECTED_EMBEDDING_MODEL=${SELECTED_EMBEDDING_MODEL:-}
- LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,DOCLING_SERVE_URL,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,ANTHROPIC_API_KEY,WATSONX_API_KEY,WATSONX_ENDPOINT,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENSEARCH_INDEX_NAME
- LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER,OPENSEARCH_PASSWORD,OPENSEARCH_URL,DOCLING_SERVE_URL,OWNER,OWNER_NAME,OWNER_EMAIL,CONNECTOR_TYPE,DOCUMENT_ID,SOURCE_URL,ALLOWED_USERS,ALLOWED_GROUPS,FILENAME,MIMETYPE,FILESIZE,SELECTED_EMBEDDING_MODEL,OPENAI_API_KEY,OPENAI_API_BASE,ANTHROPIC_API_KEY,WATSONX_API_KEY,WATSONX_ENDPOINT,WATSONX_PROJECT_ID,OLLAMA_BASE_URL,OPENSEARCH_INDEX_NAME
- LANGFLOW_LOG_LEVEL=DEBUG
- LANGFLOW_WORKERS=${LANGFLOW_WORKERS:-1}
- LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN}
Expand Down
2 changes: 1 addition & 1 deletion flows/ingestion_flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -5565,7 +5565,7 @@
"api_base": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "OpenAI API Base URL",
"display_name": "API Base URL",
"dynamic": false,
"info": "Base URL for the API. Leave empty for default.",
"input_types": [
Expand Down
22 changes: 0 additions & 22 deletions flows/openrag_agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -1806,28 +1806,6 @@
"type": "int",
"value": 100
},
"openai_api_base": {
"_input_type": "StrInput",
"advanced": true,
"display_name": "OpenAI API Base",
"dynamic": false,
"info": "The base URL of the OpenAI API. Defaults to https://api.openai.com/v1. You can change this to use other APIs like JinaChat, LocalAI and Prem.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "openai_api_base",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"output_schema": {
"_input_type": "TableInput",
"advanced": true,
Expand Down
15 changes: 14 additions & 1 deletion src/api/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import Optional

from fastapi import Depends
Expand Down Expand Up @@ -45,8 +46,20 @@ async def get_openai_models(
{"error": "OpenAI API key is required either in request body or in configuration"},
status_code=400,
)

api_base = None
try:
config = get_openrag_config()
api_base = config.providers.openai.endpoint
logger.info(
f"Retrieved OpenAI API base from config: {'yes' if api_base else 'no'}"
)
except Exception as e:
logger.error(f"Failed to get config: {e}")
if not api_base:
api_base = os.environ.get("OPENAI_API_BASE", "https://api.openai.com")

models = await models_service.get_openai_models(api_key=api_key)
models = await models_service.get_openai_models(api_key=api_key, api_base=api_base)
return JSONResponse(models)
except Exception as e:
logger.error(f"Failed to get OpenAI models: {str(e)}")
Expand Down
51 changes: 33 additions & 18 deletions src/api/provider_validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Provider validation utilities for testing API keys and models during onboarding."""

import json
import os

import httpx
from utils.container_utils import transform_localhost_url
from utils.logging_config import get_logger
Expand Down Expand Up @@ -105,7 +107,6 @@ def _extract_error_details(response: httpx.Response) -> str:
return parsed
return response_text


async def validate_provider_setup(
provider: str,
api_key: str = None,
Expand All @@ -123,7 +124,7 @@ async def validate_provider_setup(
api_key: API key for the provider (optional for ollama)
embedding_model: Embedding model to test
llm_model: LLM model to test
endpoint: Provider endpoint (required for ollama and watsonx)
endpoint: Provider endpoint (required for ollama and watsonx, optional for openai)
project_id: Project ID (required for watsonx)
test_completion: If True, performs full validation with completion/embedding tests (consumes credits).
If False, performs lightweight validation (no credits consumed). Default: False.
Expand All @@ -136,11 +137,19 @@ async def validate_provider_setup(
try:
logger.info(f"Starting validation for provider: {provider_lower} (test_completion={test_completion})")

if provider == "openai" and not endpoint:
endpoint = os.environ.get("OPENAI_API_BASE", "https://api.openai.com")

# Strip /v1 suffix from OpenAI endpoint if present to avoid double /v1 paths
if provider == "openai" and endpoint and endpoint.endswith("/v1"):
endpoint = endpoint.rstrip("/v1")
logger.info(f"Stripped /v1 suffix from OpenAI endpoint: {endpoint}")

if test_completion:
# Full validation with completion/embedding tests (consumes credits)
if embedding_model:
# Test embedding
await test_embedding(
await _test_embedding(
provider=provider_lower,
api_key=api_key,
embedding_model=embedding_model,
Expand All @@ -149,7 +158,7 @@ async def validate_provider_setup(
)
elif llm_model:
# Test completion with tool calling
await test_completion_with_tools(
await _test_completion_with_tools(
provider=provider_lower,
api_key=api_key,
llm_model=llm_model,
Expand All @@ -158,7 +167,7 @@ async def validate_provider_setup(
)
else:
# Lightweight validation (no credits consumed)
await test_lightweight_health(
await _test_lightweight_health(
provider=provider_lower,
api_key=api_key,
endpoint=endpoint,
Expand All @@ -173,7 +182,7 @@ async def validate_provider_setup(
raise


async def test_lightweight_health(
async def _test_lightweight_health(
provider: str,
api_key: str = None,
endpoint: str = None,
Expand All @@ -182,7 +191,7 @@ async def test_lightweight_health(
"""Test provider health with lightweight check (no credits consumed)."""

if provider == "openai":
await _test_openai_lightweight_health(api_key)
await _test_openai_lightweight_health(api_key, endpoint)
elif provider == "watsonx":
await _test_watsonx_lightweight_health(api_key, endpoint, project_id)
elif provider == "ollama":
Expand All @@ -193,7 +202,7 @@ async def test_lightweight_health(
raise ValueError(f"Unknown provider: {provider}")


async def test_completion_with_tools(
async def _test_completion_with_tools(
provider: str,
api_key: str = None,
llm_model: str = None,
Expand All @@ -203,7 +212,7 @@ async def test_completion_with_tools(
"""Test completion with tool calling for the provider."""

if provider == "openai":
await _test_openai_completion_with_tools(api_key, llm_model)
await _test_openai_completion_with_tools(api_key, llm_model, endpoint)
elif provider == "watsonx":
await _test_watsonx_completion_with_tools(api_key, llm_model, endpoint, project_id)
elif provider == "ollama":
Expand All @@ -214,7 +223,7 @@ async def test_completion_with_tools(
raise ValueError(f"Unknown provider: {provider}")


async def test_embedding(
async def _test_embedding(
provider: str,
api_key: str = None,
embedding_model: str = None,
Expand All @@ -224,7 +233,7 @@ async def test_embedding(
"""Test embedding generation for the provider."""

if provider == "openai":
await _test_openai_embedding(api_key, embedding_model)
await _test_openai_embedding(api_key, embedding_model, endpoint)
elif provider == "watsonx":
await _test_watsonx_embedding(api_key, embedding_model, endpoint, project_id)
elif provider == "ollama":
Expand All @@ -234,7 +243,7 @@ async def test_embedding(


# OpenAI validation functions
async def _test_openai_lightweight_health(api_key: str) -> None:
async def _test_openai_lightweight_health(api_key: str, endpoint: str) -> None:
"""Test OpenAI API key validity with lightweight check.

Only checks if the API key is valid without consuming credits.
Expand All @@ -246,10 +255,12 @@ async def _test_openai_lightweight_health(api_key: str) -> None:
"Content-Type": "application/json",
}

url = f"{endpoint}/v1/models"
logger.info("Testing openai lightweight health", url=url)
async with httpx.AsyncClient() as client:
# Use /v1/models endpoint which validates the key without consuming credits
response = await client.get(
"https://api.openai.com/v1/models",
url=url,
headers=headers,
timeout=10.0, # Short timeout for lightweight check
)
Expand All @@ -269,7 +280,7 @@ async def _test_openai_lightweight_health(api_key: str) -> None:
raise


async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> None:
async def _test_openai_completion_with_tools(api_key: str, llm_model: str, endpoint: str) -> None:
"""Test OpenAI completion with tool calling."""
try:
headers = {
Expand Down Expand Up @@ -307,8 +318,10 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
async with httpx.AsyncClient() as client:
# Try with max_tokens first
payload = {**base_payload, "max_tokens": 50}
url = f"{endpoint}/v1/chat/completions"
logger.info("Test openai completion tools", url=url)
response = await client.post(
"https://api.openai.com/v1/chat/completions",
url=url,
headers=headers,
json=payload,
timeout=30.0,
Expand All @@ -318,8 +331,9 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
if response.status_code != 200:
logger.info("max_tokens parameter failed, trying max_completion_tokens instead")
payload = {**base_payload, "max_completion_tokens": 50}
logger.info("Test openai completion tools", url=url)
response = await client.post(
"https://api.openai.com/v1/chat/completions",
url=url,
headers=headers,
json=payload,
timeout=30.0,
Expand All @@ -340,7 +354,7 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
raise


async def _test_openai_embedding(api_key: str, embedding_model: str) -> None:
async def _test_openai_embedding(api_key: str, embedding_model: str, endpoint: str) -> None:
"""Test OpenAI embedding generation."""
try:
headers = {
Expand All @@ -354,8 +368,9 @@ async def _test_openai_embedding(api_key: str, embedding_model: str) -> None:
}

async with httpx.AsyncClient() as client:
url = f"{endpoint}/v1/embeddings"
response = await client.post(
"https://api.openai.com/v1/embeddings",
url=url,
headers=headers,
json=payload,
timeout=30.0,
Expand Down
3 changes: 3 additions & 0 deletions src/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
class OpenAIConfig:
"""OpenAI provider configuration."""
api_key: str = ""
endpoint: str = ""
configured: bool = False


Expand Down Expand Up @@ -224,6 +225,8 @@ def _load_env_overrides(
# OpenAI provider settings
if os.getenv("OPENAI_API_KEY"):
config_data["providers"]["openai"]["api_key"] = os.getenv("OPENAI_API_KEY")
if os.getenv("OPENAI_API_BASE"):
config_data["providers"]["openai"]["endpoint"] = os.getenv("OPENAI_API_BASE")

# Anthropic provider settings
if os.getenv("ANTHROPIC_API_KEY"):
Expand Down
5 changes: 4 additions & 1 deletion src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,10 @@ def patched_async_client(self):
if config.providers.openai.api_key:
os.environ["OPENAI_API_KEY"] = config.providers.openai.api_key
logger.debug("Loaded OpenAI API key from config")

if config.providers.openai.endpoint:
os.environ["OPENAI_API_BASE"] = config.providers.openai.endpoint
logger.debug(f"Loaded OpenAI endpoint from config: '{config.providers.openai.endpoint}'")

# Set Anthropic credentials
if config.providers.anthropic.api_key:
os.environ["ANTHROPIC_API_KEY"] = config.providers.anthropic.api_key
Expand Down
10 changes: 8 additions & 2 deletions src/services/flows_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,11 +1393,17 @@ async def _update_component_fields(
template["api_key"]["advanced"] = False
updated = True
if provider == "openai" and "api_base" in template:
template["api_base"]["value"] = ""
template["api_base"]["load_from_db"] = False
template["api_base"]["value"] = "OPENAI_API_BASE"
template["api_base"]["load_from_db"] = True
template["api_base"]["show"] = True
template["api_base"]["advanced"] = False
updated = True
if provider == "openai" and "openai_api_base" in template:
template["openai_api_base"]["value"] = "OPENAI_API_BASE"
template["openai_api_base"]["load_from_db"] = True
template["openai_api_base"]["show"] = True
template["openai_api_base"]["advanced"] = False
updated = True

if provider == "anthropic" and "api_key" in template:
template["api_key"]["value"] = "ANTHROPIC_API_KEY"
Expand Down
6 changes: 4 additions & 2 deletions src/services/models_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ModelsService:
def __init__(self):
self.session_manager = None

async def get_openai_models(self, api_key: str) -> Dict[str, List[Dict[str, str]]]:
async def get_openai_models(self, api_key: str, api_base: str) -> Dict[str, List[Dict[str, str]]]:
"""Fetch available models from OpenAI API with lightweight validation"""
try:
headers = {
Expand All @@ -30,8 +30,10 @@ async def get_openai_models(self, api_key: str) -> Dict[str, List[Dict[str, str]
async with httpx.AsyncClient() as client:
# Lightweight validation: just check if API key is valid
# This doesn't consume credits, only validates the key
url = f"{api_base}/v1/models"
logger.debug("Getting openai models.", url=url)
response = await client.get(
"https://api.openai.com/v1/models", headers=headers, timeout=10.0
url, headers=headers, timeout=10.0
)

if response.status_code == 200:
Expand Down
8 changes: 7 additions & 1 deletion src/utils/langflow_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ def add_provider_credentials_to_headers(headers: Dict[str, str], config) -> None
# Add OpenAI credentials
if config.providers.openai.api_key:
headers["X-LANGFLOW-GLOBAL-VAR-OPENAI_API_KEY"] = str(config.providers.openai.api_key)


if config.providers.openai.endpoint:
headers["X-LANGFLOW-GLOBAL-VAR-OPENAI_API_BASE"] = str(config.providers.openai.endpoint)

# Add Anthropic credentials
if config.providers.anthropic.api_key:
headers["X-LANGFLOW-GLOBAL-VAR-ANTHROPIC_API_KEY"] = str(config.providers.anthropic.api_key)
Expand Down Expand Up @@ -47,6 +50,9 @@ def build_mcp_global_vars_from_config(config) -> Dict[str, str]:
if config.providers.openai.api_key:
global_vars["OPENAI_API_KEY"] = config.providers.openai.api_key

if config.providers.openai.endpoint:
global_vars["OPENAI_API_BASE"] = config.providers.openai.endpoint

# Add Anthropic credentials
if config.providers.anthropic.api_key:
global_vars["ANTHROPIC_API_KEY"] = config.providers.anthropic.api_key
Expand Down
Loading