feat: Add OpenAI provider support for embeddings and generation

Adds OpenAI provider to the unified provider architecture (ADR-015),
supporting:
- OpenAI API (api.openai.com)
- GitHub Models API (models.github.ai/inference)
- OpenAI-compatible endpoints (Fireworks, Together, etc.)

Features:
- Embedding support with text-embedding-3-small/large models
- Text generation via chat completions API
- Automatic retry with exponential backoff for rate limits
- Provider auto-detection in registry (priority after Bedrock)

Environment variables:
- OPENAI_API_KEY: API key (required)
- OPENAI_BASE_URL: Base URL override (optional)
- OPENAI_EMBEDDING_MODEL: Embedding model (default: text-embedding-3-small)
- OPENAI_GENERATION_MODEL: Generation model (default: gpt-4o-mini)

Also adds:
- Integration tests for RAG pipeline with MCP sampling
- MCP client sampling support for integration tests
- Ground truth Q&A pairs for Nextcloud User Manual

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-11-23 00:33:32 +01:00
co-authored by Claude
parent 959cb8b21a
commit 208365cd3d
14 changed files with 1176 additions and 21 deletions
+86
View File
@@ -259,3 +259,89 @@ class TestChunkConfigValidation:
match="DOCUMENT_CHUNK_OVERLAP .* must be less than DOCUMENT_CHUNK_SIZE",
):
get_settings()
class TestEmbeddingModelName:
"""Test get_embedding_model_name() method."""
def test_openai_takes_priority(self):
"""Test that OpenAI model is returned when OPENAI_API_KEY is set."""
settings = Settings(
openai_api_key="test-key",
openai_embedding_model="text-embedding-3-large",
ollama_base_url="http://ollama:11434",
ollama_embedding_model="nomic-embed-text",
)
assert settings.get_embedding_model_name() == "text-embedding-3-large"
def test_ollama_used_when_no_openai(self):
"""Test that Ollama model is returned when no OpenAI configured."""
settings = Settings(
ollama_base_url="http://ollama:11434",
ollama_embedding_model="all-minilm",
)
assert settings.get_embedding_model_name() == "all-minilm"
def test_simple_fallback(self):
"""Test fallback to simple provider when nothing configured."""
settings = Settings()
assert settings.get_embedding_model_name() == "simple-384"
@patch.dict(
os.environ,
{
"OPENAI_API_KEY": "test-openai-key",
"OPENAI_EMBEDDING_MODEL": "openai/text-embedding-3-small",
},
clear=True,
)
def test_get_settings_openai_model(self):
"""Test get_settings() loads OpenAI embedding model."""
settings = get_settings()
assert settings.openai_api_key == "test-openai-key"
assert settings.openai_embedding_model == "openai/text-embedding-3-small"
assert settings.get_embedding_model_name() == "openai/text-embedding-3-small"
class TestCollectionNameWithProviders:
"""Test get_collection_name() with different providers."""
def test_collection_name_with_openai(self):
"""Test collection name uses OpenAI model when configured."""
settings = Settings(
openai_api_key="test-key",
openai_embedding_model="text-embedding-3-small",
otel_service_name="my-deployment",
)
assert settings.get_collection_name() == "my-deployment-text-embedding-3-small"
def test_collection_name_with_github_models(self):
"""Test collection name sanitizes GitHub Models prefix."""
settings = Settings(
openai_api_key="ghp_test",
openai_embedding_model="openai/text-embedding-3-small",
otel_service_name="my-deployment",
)
# Slashes should be replaced with dashes
assert (
settings.get_collection_name()
== "my-deployment-openai-text-embedding-3-small"
)
def test_collection_name_with_ollama(self):
"""Test collection name uses Ollama model when no OpenAI."""
settings = Settings(
ollama_base_url="http://ollama:11434",
ollama_embedding_model="nomic-embed-text",
otel_service_name="my-deployment",
)
assert settings.get_collection_name() == "my-deployment-nomic-embed-text"
def test_collection_name_explicit_override(self):
"""Test explicit QDRANT_COLLECTION overrides auto-generation."""
settings = Settings(
qdrant_collection="custom-collection",
openai_api_key="test-key",
openai_embedding_model="text-embedding-3-large",
)
assert settings.get_collection_name() == "custom-collection"