From f6641f7e459d09c266940a2011f5b78834d1a61d Mon Sep 17 00:00:00 2001 From: GeneAI Date: Wed, 6 May 2026 23:56:34 -0400 Subject: [PATCH] chore: remove OpenAI provider and the [openai] install extra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per user vendor-preference decision: drop OpenAI as a supported provider in attune-rag. Pre-removal grep confirmed no external consumer (attune-ai, attune-author, attune-help, home Next.js app) imports OpenAIProvider — the provider is fully removable without callsite migrations. - Delete src/attune_rag/providers/openai.py and its unit tests - Drop the openai branch in providers.get_provider() - Drop "openai" from _SDK_PROBES (no longer reported by list_available) - Drop the [openai] install extra from pyproject.toml; remove openai>=1.40.0 from [all] and [dev] - Update CLI: --provider choices, install hint - Update README: drop OpenAI quickstart, install line, description - Update docstrings (pipeline.run_and_generate, base.LLMProvider) - CHANGELOG entry under [Unreleased] Test plan: 298 passed, 3 xpassed (full suite); 14/14 providers tests pass with the dispatch test updated to reflect the new {claude, gemini} contract. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 7 +++ README.md | 15 +------ pyproject.toml | 6 +-- src/attune_rag/cli.py | 13 ++++-- src/attune_rag/pipeline.py | 6 +-- src/attune_rag/providers/__init__.py | 6 --- src/attune_rag/providers/base.py | 2 +- src/attune_rag/providers/openai.py | 53 ---------------------- tests/unit/providers/test_dispatch.py | 2 +- tests/unit/providers/test_openai.py | 65 --------------------------- 10 files changed, 23 insertions(+), 152 deletions(-) delete mode 100644 src/attune_rag/providers/openai.py delete mode 100644 tests/unit/providers/test_openai.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b6529..4294307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **OpenAI provider** (`attune_rag.providers.openai.OpenAIProvider`) and + the `[openai]` install extra. Use `[claude]` or `[gemini]` instead. + No external consumer of attune-rag was importing `OpenAIProvider`; + the provider is fully removable without callsite migrations. + ## [0.1.12] - 2026-05-05 ### Added diff --git a/README.md b/README.md index eace592..40d811f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # attune-rag Lightweight, LLM-agnostic RAG pipeline with pluggable -corpora. Works with Claude, OpenAI, Gemini, or any LLM. +corpora. Works with Claude, Gemini, or any LLM. - **No LLM SDK at install time.** All provider deps are optional extras. @@ -21,7 +21,6 @@ corpora. Works with Claude, OpenAI, Gemini, or any LLM. pip install attune-rag # core only pip install 'attune-rag[attune-help]' # + bundled help corpus pip install 'attune-rag[claude]' # + Claude adapter -pip install 'attune-rag[openai]' # + OpenAI adapter pip install 'attune-rag[gemini]' # + Gemini adapter pip install 'attune-rag[all]' # everything ``` @@ -48,18 +47,6 @@ async def main(): asyncio.run(main()) ``` -## Quick start — OpenAI - -```bash -pip install 'attune-rag[attune-help,openai]' -``` - -```python -response, result = await pipeline.run_and_generate( - "...", provider="openai", model="gpt-4o", -) -``` - ## Quick start — Gemini ```bash diff --git a/pyproject.toml b/pyproject.toml index 2c2ea16..90ac0e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "attune-rag" version = "0.1.12" -description = "Lightweight, LLM-agnostic RAG pipeline with pluggable corpora. Works with Claude, OpenAI, Gemini, or any LLM." +description = "Lightweight, LLM-agnostic RAG pipeline with pluggable corpora. Works with Claude, Gemini, or any LLM." readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3.10" license = {file = "LICENSE"} @@ -17,7 +17,6 @@ keywords = [ "retrieval-augmented-generation", "llm", "claude", - "openai", "gemini", "grounding", "provenance", @@ -46,12 +45,10 @@ dependencies = [ [project.optional-dependencies] attune-help = ["attune-help>=0.10.0,<0.11"] claude = ["anthropic>=0.40.0,<1.0"] -openai = ["openai>=1.40.0,<2.0"] gemini = ["google-genai>=1.0,<2.0"] all = [ "attune-help>=0.10.0,<0.11", "anthropic>=0.40.0,<1.0", - "openai>=1.40.0,<2.0", "google-genai>=1.0,<2.0", ] dev = [ @@ -64,7 +61,6 @@ dev = [ "twine>=5.0", "attune-help>=0.10.0,<0.11", "anthropic>=0.40.0,<1.0", - "openai>=1.40.0,<2.0", "google-genai>=1.0,<2.0", ] diff --git a/src/attune_rag/cli.py b/src/attune_rag/cli.py index be13efc..cbb2e3e 100644 --- a/src/attune_rag/cli.py +++ b/src/attune_rag/cli.py @@ -92,7 +92,7 @@ def _cmd_providers(args: argparse.Namespace) -> int: available = list_available() if not available: print("No provider extras installed.") - print("Install one: pip install 'attune-rag[claude]' (or openai, gemini).") + print("Install one: pip install 'attune-rag[claude]' (or gemini).") return 0 print("Available providers:") for name in available: @@ -114,6 +114,7 @@ def _cmd_dashboard_render(args: argparse.Namespace) -> int: print(f"Dashboard written to {out}") if args.open: import webbrowser + webbrowser.open(out.as_uri()) return 0 @@ -148,7 +149,7 @@ def build_parser() -> argparse.ArgumentParser: query.add_argument("-k", type=int, default=3, help="Max hits to return (default 3).") query.add_argument( "--provider", - choices=["claude", "openai", "gemini"], + choices=["claude", "gemini"], help="If set, call the named LLM and print its response.", ) query.add_argument( @@ -172,7 +173,9 @@ def build_parser() -> argparse.ArgumentParser: dash = subs.add_parser("dashboard", help="Render or refresh the attune-rag Cowork dashboard.") dash_subs = dash.add_subparsers(dest="dashboard_cmd", required=True) - show_p = dash_subs.add_parser("show", help="Run benchmark and display dashboard in the terminal.") + show_p = dash_subs.add_parser( + "show", help="Run benchmark and display dashboard in the terminal." + ) show_p.add_argument( "--corpus-package", default="attune_help", @@ -181,7 +184,9 @@ def build_parser() -> argparse.ArgumentParser: ) show_p.set_defaults(func=_cmd_dashboard_show) - render_p = dash_subs.add_parser("render", help="Run benchmark, embed snapshot, write dashboard HTML.") + render_p = dash_subs.add_parser( + "render", help="Run benchmark, embed snapshot, write dashboard HTML." + ) render_p.add_argument("--out", required=True, metavar="PATH", help="Destination file path.") render_p.add_argument( "--corpus-package", diff --git a/src/attune_rag/pipeline.py b/src/attune_rag/pipeline.py index 2b2fcc7..2538c52 100644 --- a/src/attune_rag/pipeline.py +++ b/src/attune_rag/pipeline.py @@ -209,9 +209,9 @@ async def run_and_generate( """Retrieve, build the augmented prompt, and call an LLM. ``provider`` may be an ``LLMProvider`` instance or a name - string (``"claude"``, ``"openai"``, ``"gemini"``). In the - string case, the matching provider is constructed with - default credentials (from env vars). + string (``"claude"``, ``"gemini"``). In the string case, + the matching provider is constructed with default + credentials (from env vars). ``prompt_variant`` selects the prompt template. See :mod:`attune_rag.prompts`. diff --git a/src/attune_rag/providers/__init__.py b/src/attune_rag/providers/__init__.py index 2e6556a..a290eb0 100644 --- a/src/attune_rag/providers/__init__.py +++ b/src/attune_rag/providers/__init__.py @@ -3,7 +3,6 @@ Each adapter is behind a pip extra: - attune-rag[claude] -> ClaudeProvider -- attune-rag[openai] -> OpenAIProvider - attune-rag[gemini] -> GeminiProvider Adapters lazy-import their SDKs so attune-rag installs @@ -18,7 +17,6 @@ _SDK_PROBES = { "claude": "anthropic", - "openai": "openai", "gemini": "google.genai", } @@ -51,10 +49,6 @@ def get_provider(name: str, **kwargs) -> LLMProvider: from .claude import ClaudeProvider return ClaudeProvider(**kwargs) - if name == "openai": - from .openai import OpenAIProvider - - return OpenAIProvider(**kwargs) if name == "gemini": from .gemini import GeminiProvider diff --git a/src/attune_rag/providers/base.py b/src/attune_rag/providers/base.py index 5593334..2973354 100644 --- a/src/attune_rag/providers/base.py +++ b/src/attune_rag/providers/base.py @@ -9,7 +9,7 @@ class LLMProvider(Protocol): """An async LLM provider that consumes a prompt and returns text. - Implementations live in ``attune_rag.providers.{claude,openai,gemini}`` + Implementations live in ``attune_rag.providers.{claude,gemini}`` behind optional extras. Each lazy-imports its SDK so core attune-rag installs cleanly without any provider deps. """ diff --git a/src/attune_rag/providers/openai.py b/src/attune_rag/providers/openai.py deleted file mode 100644 index 007a733..0000000 --- a/src/attune_rag/providers/openai.py +++ /dev/null @@ -1,53 +0,0 @@ -"""OpenAIProvider — requires ``attune-rag[openai]`` extra.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from openai import AsyncOpenAI - - -class OpenAIProvider: - """Thin async wrapper over OpenAI's chat completions API. - - Lazy-imports ``openai`` so attune-rag installs cleanly - without the OpenAI SDK. - """ - - name = "openai" - DEFAULT_MODEL = "gpt-4o" - - def __init__( - self, - api_key: str | None = None, - client: AsyncOpenAI | None = None, - ) -> None: - if client is not None: - self._client = client - return - try: - from openai import AsyncOpenAI - except ImportError as exc: - raise RuntimeError( - "OpenAIProvider requires the [openai] extra. " - "Install with: pip install 'attune-rag[openai]'" - ) from exc - self._client = AsyncOpenAI(api_key=api_key) - - async def generate( - self, - prompt: str, - model: str | None = None, - max_tokens: int = 2048, - cached_prefix: ( - str | None - ) = None, # noqa: ARG002 — protocol parity; OpenAI has no equivalent yet - ) -> str: - response = await self._client.chat.completions.create( - model=model or self.DEFAULT_MODEL, - max_tokens=max_tokens, - messages=[{"role": "user", "content": prompt}], - ) - choice = response.choices[0] - return choice.message.content or "" diff --git a/tests/unit/providers/test_dispatch.py b/tests/unit/providers/test_dispatch.py index a4e4f93..e468820 100644 --- a/tests/unit/providers/test_dispatch.py +++ b/tests/unit/providers/test_dispatch.py @@ -13,7 +13,7 @@ def test_list_available_returns_installed_providers() -> None: # rather than a specific list. assert isinstance(available, list) for name in available: - assert name in {"claude", "openai", "gemini"} + assert name in {"claude", "gemini"} def test_get_provider_rejects_unknown_name() -> None: diff --git a/tests/unit/providers/test_openai.py b/tests/unit/providers/test_openai.py deleted file mode 100644 index 1adc192..0000000 --- a/tests/unit/providers/test_openai.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Unit tests for OpenAIProvider.""" - -from __future__ import annotations - -import asyncio -import sys -from unittest.mock import AsyncMock, MagicMock - -import pytest - - -def _build_response(text: str) -> MagicMock: - choice = MagicMock() - choice.message.content = text - response = MagicMock() - response.choices = [choice] - return response - - -def test_generate_passes_prompt_to_client() -> None: - from attune_rag.providers.openai import OpenAIProvider - - client = MagicMock() - client.chat.completions.create = AsyncMock(return_value=_build_response("hello")) - provider = OpenAIProvider(client=client) - result = asyncio.run(provider.generate("prompt")) - assert result == "hello" - kwargs = client.chat.completions.create.await_args.kwargs - assert kwargs["messages"] == [{"role": "user", "content": "prompt"}] - assert kwargs["model"] == OpenAIProvider.DEFAULT_MODEL - - -def test_generate_respects_model_override() -> None: - from attune_rag.providers.openai import OpenAIProvider - - client = MagicMock() - client.chat.completions.create = AsyncMock(return_value=_build_response("ok")) - provider = OpenAIProvider(client=client) - asyncio.run(provider.generate("q", model="gpt-4-turbo")) - assert client.chat.completions.create.await_args.kwargs["model"] == "gpt-4-turbo" - - -def test_missing_sdk_raises_helpful_error() -> None: - """sys.modules sentinel — works across Python 3.10-3.13.""" - saved: dict[str, object] = {} - for key in list(sys.modules): - if key in {"openai", "attune_rag.providers.openai"} or key.startswith("openai."): - saved[key] = sys.modules.pop(key) - - sys.modules["openai"] = None # type: ignore[assignment] - - try: - from attune_rag.providers.openai import OpenAIProvider - - with pytest.raises(RuntimeError, match=r"\[openai\] extra"): - OpenAIProvider() - finally: - sys.modules.pop("openai", None) - sys.modules.update(saved) - - -def test_provider_name() -> None: - from attune_rag.providers.openai import OpenAIProvider - - assert OpenAIProvider.name == "openai"