diff --git a/docker-compose.yml b/docker-compose.yml index b705ee106..92193b132 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} @@ -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} @@ -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} diff --git a/flows/ingestion_flow.json b/flows/ingestion_flow.json index c5c2fc375..6cfcb289f 100644 --- a/flows/ingestion_flow.json +++ b/flows/ingestion_flow.json @@ -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": [ diff --git a/flows/openrag_agent.json b/flows/openrag_agent.json index b2f41b1b6..2c81cd51e 100644 --- a/flows/openrag_agent.json +++ b/flows/openrag_agent.json @@ -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, diff --git a/src/api/models.py b/src/api/models.py index c99d06397..be3a084b4 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,3 +1,4 @@ +import os from typing import Optional from fastapi import Depends @@ -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)}") diff --git a/src/api/provider_validation.py b/src/api/provider_validation.py index dd96912aa..9baad07c7 100644 --- a/src/api/provider_validation.py +++ b/src/api/provider_validation.py @@ -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 @@ -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, @@ -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. @@ -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, @@ -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, @@ -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, @@ -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, @@ -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": @@ -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, @@ -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": @@ -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, @@ -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": @@ -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. @@ -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 ) @@ -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 = { @@ -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, @@ -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, @@ -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 = { @@ -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, diff --git a/src/config/config_manager.py b/src/config/config_manager.py index 8b3bb38b8..5f8362803 100644 --- a/src/config/config_manager.py +++ b/src/config/config_manager.py @@ -14,6 +14,7 @@ class OpenAIConfig: """OpenAI provider configuration.""" api_key: str = "" + endpoint: str = "" configured: bool = False @@ -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"): diff --git a/src/config/settings.py b/src/config/settings.py index b4d1e9e28..740badb6e 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -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 diff --git a/src/services/flows_service.py b/src/services/flows_service.py index e97ac2d3a..9a0614b69 100644 --- a/src/services/flows_service.py +++ b/src/services/flows_service.py @@ -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" diff --git a/src/services/models_service.py b/src/services/models_service.py index 1bc77d833..95271ae0b 100644 --- a/src/services/models_service.py +++ b/src/services/models_service.py @@ -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 = { @@ -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: diff --git a/src/utils/langflow_headers.py b/src/utils/langflow_headers.py index e3447e611..bfc5f1271 100644 --- a/src/utils/langflow_headers.py +++ b/src/utils/langflow_headers.py @@ -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) @@ -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