Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5577ae8
feat: Add comprehensive OpenAI base URL configuration support
orestesgarcia Sep 4, 2025
39f753a
fix: Address PR review feedback for OpenAI base URL configuration
orestesgarcia Sep 4, 2025
e06fbc6
Update python/src/agents/agent_provider_config.py
orestesgarcia Sep 4, 2025
6db9b19
Update python/src/agents/base_agent.py
orestesgarcia Sep 4, 2025
c4cf1c7
Update python/src/agents/agent_provider_config.py
orestesgarcia Sep 4, 2025
c462c18
Update python/src/agents/agent_provider_config.py
orestesgarcia Sep 4, 2025
1091e1b
docs: Add OpenAI base URL configuration documentation
orestesgarcia Sep 4, 2025
f7fcf77
feat: Centralize OpenAI client configuration across all services
orestesgarcia Sep 4, 2025
a4e1689
docs: Update documentation to reflect centralized OpenAI configuration
orestesgarcia Sep 4, 2025
9da7379
Update docs/docs/api-reference.mdx
orestesgarcia Sep 4, 2025
8b71633
Update docs/docs/rag.mdx
orestesgarcia Sep 4, 2025
54d29a3
Update python/src/agents/agent_provider_config.py
orestesgarcia Sep 4, 2025
efb5f20
Update python/src/server/services/llm_provider_service.py
orestesgarcia Sep 4, 2025
2f26bf6
fix: Improve error logging in LLM provider service
orestesgarcia Sep 4, 2025
1a770ce
feat: Improve error handling and observability in base agent
orestesgarcia Sep 4, 2025
3c53fde
feat: Improve private host detection for HTTP URL warnings
orestesgarcia Sep 4, 2025
58a9e13
feat: Add configurable timeouts and retries to OpenAI clients
orestesgarcia Sep 4, 2025
8604252
Update docs/docs/rag.mdx
orestesgarcia Sep 4, 2025
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
15 changes: 15 additions & 0 deletions archon-ui-main/src/components/settings/RAGSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface RAGSettingsProps {
USE_RERANKING: boolean;
LLM_PROVIDER?: string;
LLM_BASE_URL?: string;
OPENAI_BASE_URL?: string;
EMBEDDING_MODEL?: string;
// Crawling Performance Settings
CRAWL_BATCH_SIZE?: number;
Expand Down Expand Up @@ -85,6 +86,20 @@ export const RAGSettings = ({
/>
</div>
)}
{ragSettings.LLM_PROVIDER === 'openai' && (
<div>
<Input
label="OpenAI Base URL (optional)"
value={ragSettings.OPENAI_BASE_URL || ''}
onChange={e => setRagSettings({
...ragSettings,
OPENAI_BASE_URL: e.target.value
})}
placeholder="https://api.openai.com/v1"
accentColor="green"
/>
</div>
)}
<div className="flex items-end">
<Button
variant="outline"
Expand Down
131 changes: 131 additions & 0 deletions python/src/agents/agent_provider_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Agent Provider Configuration

Handles OpenAI provider configuration for PydanticAI agents.
Enables custom base_url configuration for OpenAI-compatible endpoints.
"""

import logging
import os

from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

logger = logging.getLogger(__name__)


async def get_configured_openai_model(model_name: str) -> OpenAIChatModel | str:
"""
Get a configured OpenAI model for PydanticAI agents.

If OPENAI_BASE_URL is configured in the system, returns an OpenAIChatModel
with a custom OpenAIProvider. Otherwise, returns the standard model string
format that PydanticAI handles automatically.

Args:
model_name: The model name (e.g., "gpt-4o", "gpt-4o-mini")

Returns:
Either an OpenAIChatModel with custom provider or a model string
"""
Comment thread
orestesgarcia marked this conversation as resolved.
try:
# Try to get base URL from credential service
base_url = await _get_openai_base_url()

if base_url:
# Get API key
api_key = await _get_openai_api_key()
if not api_key:
logger.warning("OPENAI_BASE_URL is configured but no API key found, falling back to default")
return f"openai:{model_name}"

# Create custom provider with base_url
provider = OpenAIProvider(
base_url=base_url,
api_key=api_key
)

logger.info(f"Creating OpenAI model {model_name} with custom base URL: {base_url}")
return OpenAIChatModel(model_name, provider=provider)
else:
# No custom base URL, use standard string format
logger.debug(f"Using default OpenAI configuration for model: {model_name}")
return f"openai:{model_name}"

except Exception as e:
logger.error(f"Error configuring OpenAI model: {e}")
# Fallback to standard string format
return f"openai:{model_name}"

Comment thread
orestesgarcia marked this conversation as resolved.
Comment thread
orestesgarcia marked this conversation as resolved.
Outdated

async def _get_openai_base_url() -> str | None:
"""Get OpenAI base URL from credential service."""
try:
# Import here to avoid circular imports
from ..server.services.credential_service import credential_service

# Get RAG settings which contain OPENAI_BASE_URL
rag_settings = await credential_service.get_credentials_by_category("rag_strategy")
base_url = rag_settings.get("OPENAI_BASE_URL")

if base_url:
logger.debug(f"Found OPENAI_BASE_URL in settings: {base_url}")
return base_url
else:
# Check environment variable as fallback
env_base_url = os.getenv("OPENAI_BASE_URL")
if env_base_url:
logger.debug(f"Found OPENAI_BASE_URL in environment: {env_base_url}")
return env_base_url

Comment thread
orestesgarcia marked this conversation as resolved.
Outdated
return None

except Exception as e:
logger.debug(f"Could not get OPENAI_BASE_URL from settings: {e}")
# Try environment variable as fallback
return os.getenv("OPENAI_BASE_URL")


async def _get_openai_api_key() -> str | None:
"""Get OpenAI API key from credential service."""
try:
# Import here to avoid circular imports
from ..server.services.credential_service import credential_service

# Try to get from credential service first
api_key = await credential_service.get_credential("OPENAI_API_KEY", decrypt=True)
if api_key:
logger.debug("Found OPENAI_API_KEY in credential service")
return api_key

# Fallback to environment variable
env_api_key = os.getenv("OPENAI_API_KEY")
if env_api_key:
logger.debug("Found OPENAI_API_KEY in environment")
return env_api_key

return None

except Exception as e:
logger.debug(f"Could not get OPENAI_API_KEY from settings: {e}")
# Try environment variable as fallback
return os.getenv("OPENAI_API_KEY")


def get_configured_openai_model_sync(model_name: str) -> str:
"""
Synchronous version that returns model string format.

Since we can't easily call async functions from sync contexts,
this returns the standard model string format and relies on
PydanticAI's automatic configuration.

Args:
model_name: The model name (e.g., "gpt-4o", "gpt-4o-mini")

Returns:
Model string in "openai:model" format
"""
# For synchronous contexts, we use the standard format
# PydanticAI will handle OpenAI configuration automatically
return f"openai:{model_name}"
39 changes: 34 additions & 5 deletions python/src/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,29 +156,53 @@ def __init__(
name: str = None,
retries: int = 3,
enable_rate_limiting: bool = True,
use_custom_provider: bool = True,
**agent_kwargs,
):
self.model = model
self.name = name or self.__class__.__name__
self.retries = retries
self.enable_rate_limiting = enable_rate_limiting
self.use_custom_provider = use_custom_provider

# Initialize rate limiting
if self.enable_rate_limiting:
self.rate_limiter = RateLimitHandler(max_retries=retries)
else:
self.rate_limiter = None

# Initialize the PydanticAI agent
self._agent = self._create_agent(**agent_kwargs)

# Setup logging
self.logger = logging.getLogger(f"agents.{self.name}")

# Initialize the PydanticAI agent (this may be async now)
self._agent = None
self._agent_kwargs = agent_kwargs

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
@abstractmethod
def _create_agent(self, **kwargs) -> Agent:
async def _create_agent(self, **kwargs) -> Agent:
"""Create and configure the PydanticAI agent. Must be implemented by subclasses."""
pass

async def _ensure_agent_initialized(self):
"""Ensure the PydanticAI agent is initialized."""
if self._agent is None:
self._agent = await self._create_agent(**self._agent_kwargs)

async def _get_configured_model(self):
"""Get the configured model for this agent."""
if self.use_custom_provider and self.model.startswith("openai:"):
# Extract model name from "openai:model" format
model_name = self.model.replace("openai:", "")

try:
from .agent_provider_config import get_configured_openai_model
return await get_configured_openai_model(model_name)
except Exception as e:
self.logger.warning(f"Failed to get custom OpenAI provider, falling back to default: {e}")
return self.model
Comment thread
orestesgarcia marked this conversation as resolved.
else:
# Use the model string as-is
return self.model
Comment thread
orestesgarcia marked this conversation as resolved.

@abstractmethod
def get_system_prompt(self) -> str:
Expand Down Expand Up @@ -208,6 +232,9 @@ async def run(self, user_prompt: str, deps: DepsT) -> OutputT:
async def _run_agent(self, user_prompt: str, deps: DepsT) -> OutputT:
"""Internal method to run the agent."""
try:
# Ensure agent is initialized
await self._ensure_agent_initialized()

# Add timeout to prevent hanging
result = await asyncio.wait_for(
self._agent.run(user_prompt, deps=deps),
Expand All @@ -223,7 +250,7 @@ async def _run_agent(self, user_prompt: str, deps: DepsT) -> OutputT:
self.logger.error(f"Agent {self.name} failed: {str(e)}")
raise

def run_stream(self, user_prompt: str, deps: DepsT):
async def run_stream(self, user_prompt: str, deps: DepsT):
"""
Run the agent with streaming output.

Expand All @@ -236,6 +263,8 @@ def run_stream(self, user_prompt: str, deps: DepsT):
"""
# Note: Rate limiting not supported for streaming to avoid complexity
# The async context manager pattern doesn't work well with rate limiting
await self._ensure_agent_initialized()

self.logger.info(f"Starting streaming for agent {self.name}")
# run_stream returns an async context manager directly, not a coroutine
return self._agent.run_stream(user_prompt, deps=deps)
Expand Down
7 changes: 5 additions & 2 deletions python/src/agents/document_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@ def __init__(self, model: str = None, **kwargs):
model=model, name="DocumentAgent", retries=3, enable_rate_limiting=True, **kwargs
)

def _create_agent(self, **kwargs) -> Agent:
async def _create_agent(self, **kwargs) -> Agent:
"""Create the PydanticAI agent with tools and prompts."""

# Get the configured model (may be custom provider or default)
configured_model = await self._get_configured_model()

agent = Agent(
model=self.model,
model=configured_model,
deps_type=DocumentDependencies,
result_type=DocumentOperation,
system_prompt="""You are a Document Management Assistant that helps users create, update, and modify project documents through conversation.
Expand Down
7 changes: 5 additions & 2 deletions python/src/agents/rag_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,14 @@ def __init__(self, model: str = None, **kwargs):
model=model, name="RagAgent", retries=3, enable_rate_limiting=True, **kwargs
)

def _create_agent(self, **kwargs) -> Agent:
async def _create_agent(self, **kwargs) -> Agent:
"""Create the PydanticAI agent with tools and prompts."""

# Get the configured model (may be custom provider or default)
configured_model = await self._get_configured_model()

agent = Agent(
model=self.model,
model=configured_model,
deps_type=RagDependencies,
system_prompt="""You are a RAG (Retrieval-Augmented Generation) Assistant that helps users search and understand documentation through conversation.

Expand Down
3 changes: 2 additions & 1 deletion python/src/agents/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ async def generate() -> AsyncGenerator[str, None]:

# Use PydanticAI's run_stream method
# run_stream returns an async context manager directly
async with agent.run_stream(request.prompt, deps) as stream:
stream_cm = await agent.run_stream(request.prompt, deps)
async with stream_cm as stream:
# Stream text chunks as they arrive
async for chunk in stream.stream_text():
event_data = json.dumps({"type": "stream_chunk", "content": chunk})
Expand Down
3 changes: 3 additions & 0 deletions python/src/server/services/credential_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,9 @@ def _get_provider_base_url(self, provider: str, rag_settings: dict) -> str | Non
return rag_settings.get("LLM_BASE_URL", "http://localhost:11434/v1")
elif provider == "google":
return "https://generativelanguage.googleapis.com/v1beta/openai/"
elif provider == "openai":
# Allow custom OpenAI-compatible endpoint
return rag_settings.get("OPENAI_BASE_URL")
return None # Use default for OpenAI

async def set_active_provider(self, provider: str, service_type: str = "llm") -> bool:
Expand Down
11 changes: 9 additions & 2 deletions python/src/server/services/llm_provider_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,15 @@ async def get_llm_client(provider: str | None = None, use_embedding_provider: bo
if not api_key:
raise ValueError("OpenAI API key not found")

client = openai.AsyncOpenAI(api_key=api_key)
logger.info("OpenAI client created successfully")
# Create client with optional base_url for custom OpenAI-compatible endpoints
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url
logger.info(f"OpenAI client created with custom base URL: {base_url}")
else:
logger.info("OpenAI client created with default URL")

client = openai.AsyncOpenAI(**client_kwargs)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
elif provider_name == "ollama":
# Ollama requires an API key in the client but doesn't actually use it
Expand Down
30 changes: 20 additions & 10 deletions python/src/server/services/storage/code_storage_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,26 +542,36 @@ def generate_code_example_summary(
import openai

api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
# Try to get from credential service with direct fallback
from ..credential_service import credential_service
base_url = None

if (
credential_service._cache_initialized
and "OPENAI_API_KEY" in credential_service._cache
):
# Try to get from credential service with direct fallback
from ..credential_service import credential_service

if credential_service._cache_initialized:
# Get API key from cache if needed
if not api_key and "OPENAI_API_KEY" in credential_service._cache:
cached_key = credential_service._cache["OPENAI_API_KEY"]
if isinstance(cached_key, dict) and cached_key.get("is_encrypted"):
api_key = credential_service._decrypt_value(cached_key["encrypted_value"])
else:
api_key = cached_key
else:
api_key = os.getenv("OPENAI_API_KEY", "")

# Get base URL from cache if available (check RAG settings cache)
if hasattr(credential_service, '_rag_settings_cache') and credential_service._rag_settings_cache:
base_url = credential_service._rag_settings_cache.get("OPENAI_BASE_URL")

if not api_key:
api_key = os.getenv("OPENAI_API_KEY", "")

if not api_key:
raise ValueError("No OpenAI API key available")

client = openai.OpenAI(api_key=api_key)
# Create client with optional base_url for custom OpenAI-compatible endpoints
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url

client = openai.OpenAI(**client_kwargs)
except Exception as e:
Comment thread
orestesgarcia marked this conversation as resolved.
Outdated
search_logger.error(
f"Failed to create LLM client fallback: {e} - returning default values"
Expand Down