Skip to content

feat(adapters): OpenAI-compatible LLM adapter - #550

Merged
frankbria merged 2 commits into
mainfrom
feat/543-openai-llm-adapter
Apr 4, 2026
Merged

feat(adapters): OpenAI-compatible LLM adapter#550
frankbria merged 2 commits into
mainfrom
feat/543-openai-llm-adapter

Conversation

@frankbria

@frankbria frankbria commented Apr 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements OpenAIProvider(LLMProvider) in codeframe/adapters/llm/openai.py — mirrors AnthropicProvider structure exactly
  • A single base_url constructor param covers the entire OpenAI-compatible ecosystem: OpenAI, Ollama, vLLM, LM Studio, Groq, Together, etc.
  • Updates codeframe/adapters/llm/__init__.py to export OpenAIProvider and wire get_provider("openai")
  • Adds tests/adapters/test_llm_openai.py with 22 tests covering init, complete, stream, base_url routing, error handling, tool round-trip, and message conversion

Closes #543 | Part of #542

Acceptance criteria

  • OpenAIProvider passes same interface contract as AnthropicProvider
  • base_url override routes to custom endpoint (tested: Ollama pattern)
  • Tool use round-trip works (tool call → tool result → final answer)
  • All new tests pass (22/22), existing tests unaffected (81/81)
  • Works with real OpenAI API key in integration test (requires live key — not in CI)
  • Works with Ollama via base_url override (requires live Ollama instance)

Test plan

  • uv run pytest tests/adapters/test_llm_openai.py — 22 passed
  • uv run pytest tests/adapters/ — 81 passed (no regressions)
  • uv run ruff check codeframe/adapters/llm/openai.py tests/adapters/test_llm_openai.py — clean

Summary by CodeRabbit

  • New Features

    • Added OpenAI provider so the app can call OpenAI chat-completion endpoints.
    • API key resolution via credential manager, explicit argument, or environment variable.
    • Configurable default model, optional custom base URL, tool-function calling support, and streaming responses.
  • Tests

    • Added comprehensive tests covering initialization, key resolution, tool-call flows, streaming, model routing, and error handling.

Add OpenAIProvider implementing the LLMProvider ABC with full support
for OpenAI and any OpenAI-compatible endpoint (Ollama, vLLM, Groq, etc.)
via a configurable base_url. Includes tool use, streaming, stop-reason
mapping, and credential manager integration.

- Create codeframe/adapters/llm/openai.py — OpenAIProvider(LLMProvider)
- Update codeframe/adapters/llm/__init__.py — export + get_provider("openai")
- Create tests/adapters/test_llm_openai.py — 22 tests, all passing

Closes #543
@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds an OpenAI-compatible LLM adapter: new OpenAIProvider implementing LLMProvider, OpenAI SDK integration with configurable base_url, message/tool format conversion, streaming support, and factory export adjustment to instantiate it.

Changes

Cohort / File(s) Summary
Provider exports & factory
codeframe/adapters/llm/__init__.py
Export OpenAIProvider and extend get_provider() to accept provider_type="openai" and return OpenAIProvider().
OpenAI adapter implementation
codeframe/adapters/llm/openai.py
New OpenAIProvider class: API key resolution (credential manager → arg → env), lazy openai.OpenAI client with base_url, get_model(), complete() (message/tool ↔ OpenAI chat format, function-calling mapping, error translation), stream() streaming support, and response parsing including token usage and stop-reason mapping.
Tests
tests/adapters/test_llm_openai.py
New comprehensive tests covering initialization (API key sourcing, base_url, model), credential-manager integration, complete() mappings (text, tool-call round-trip, system prepending, temperature), stream() behavior, message conversion, base_url client construction, and error handling.

Sequence Diagram

sequenceDiagram
    participant Client
    participant OpenAIProvider
    participant CredentialMgr
    participant OpenAI_SDK
    participant OpenAI_API

    Client->>OpenAIProvider: initialize(api_key?, base_url?, model?, credential_manager?)
    OpenAIProvider->>CredentialMgr: credential_manager.get_credential() (if provided)
    OpenAIProvider-->>Client: resolved api_key / error

    Client->>OpenAIProvider: complete(messages, tools, system?, temperature, max_tokens)
    OpenAIProvider->>OpenAIProvider: _convert_messages() / _convert_tools()
    OpenAIProvider->>OpenAI_SDK: chat.completions.create(model, messages, tools..., stream=False)
    OpenAI_SDK->>OpenAI_API: POST /chat/completions
    OpenAI_API-->>OpenAI_SDK: response (choices, usage, finish_reason)
    OpenAI_SDK-->>OpenAIProvider: response
    OpenAIProvider->>OpenAIProvider: parse response (content, tool_calls, tokens, stop_reason)
    OpenAIProvider-->>Client: LLMResponse (content/tool_calls/tokens/stop_reason)

    Client->>OpenAIProvider: stream(messages...)
    OpenAIProvider->>OpenAI_SDK: chat.completions.create(..., stream=True)
    OpenAI_SDK->>OpenAI_API: streaming events
    OpenAI_API-->>OpenAI_SDK: stream chunks
    OpenAI_SDK-->>OpenAIProvider: stream chunks
    OpenAIProvider-->>Client: yield text chunks
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I toggled keys and stitched the flow,
Tools to functions, messages grow,
A base_url bent to local light,
Streams that dance through day and night,
Hooray — one provider hops just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(adapters): OpenAI-compatible LLM adapter' accurately and concisely summarizes the main change—adding an OpenAI provider implementation to the adapters module.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #543: OpenAIProvider implementation with complete() and stream() methods, tool round-trip support, base_url routing, error handling, and 22 comprehensive tests all demonstrate compliance.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #543 requirements: OpenAIProvider implementation, test coverage, and init.py wiring for the provider. No extraneous modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 88.57% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/543-openai-llm-adapter

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Apr 4, 2026

Copy link
Copy Markdown

Review: feat(adapters): OpenAI-compatible LLM adapter

Solid implementation that mirrors AnthropicProvider cleanly and covers the OpenAI-compatible ecosystem well. The lazy client init, key resolution order, and message conversion logic are all correct. 22 tests is good coverage. A few issues worth addressing before merge:


Bug: temperature=0.0 is silently dropped

Both complete() and stream() only forward temperature when it is > 0:

if temperature > 0:
    kwargs["temperature"] = temperature

The method signature defaults to temperature: float = 0.0. OpenAI's own default temperature is 1.0, so any caller that expects deterministic output (the common case) will silently get temperature=1.0 instead. This should unconditionally include temperature, or at minimum always include it when the caller explicitly provides it:

kwargs["temperature"] = temperature  # always send; let OpenAI decide if it matters

Missing dependency: openai not added to pyproject.toml

The diff adds import openai at the top of openai.py but there is no pyproject.toml change in this PR. If openai is not already a declared dependency, users who install the package without it will hit an ImportError at import time. Consider either adding it as a core dependency or as an optional extra (e.g., codeframe[openai]) with a lazy import guard, consistent with the e2b adapter pattern.


stream() has no error handling

complete() catches AuthenticationError, RateLimitError, and NotFoundError and re-raises as ValueError. stream() does not, so those exceptions will surface as raw openai.* exceptions. Callers that only handle ValueError will see uncaught errors. The same try/except block should wrap the streaming call.


get_provider("openai") has no way to pass base_url

elif provider_type == "openai":
    return OpenAIProvider()

The entire Ollama/vLLM use case depends on base_url, but the factory function provides no way to pass it. This limits the utility of get_provider for non-OpenAI endpoints. A simple fix is accepting **kwargs and forwarding them, or accepting a base_url param:

def get_provider(provider_type: str = "anthropic", **kwargs) -> LLMProvider:
    ...
    elif provider_type == "openai":
        return OpenAIProvider(**kwargs)

Error tests accept raw exceptions — masks potential regression

with pytest.raises((ValueError, openai.AuthenticationError)):

The implementation explicitly wraps these into ValueError, so the test should assert specifically ValueError. Accepting either means a regression (the wrapping being removed) would still pass the test.


Minor: CredentialProvider.LLM_OPENAI — verify this constant exists

The provider imports CredentialProvider.LLM_OPENAI from codeframe.core.credentials inside __init__. Since the PR doesn't touch credentials.py, confirm this constant is already defined there, otherwise this code path will fail at runtime.


Minor: model_selector silently ignored

The docstring states the parameter is ignored. Consider raising ValueError (or a warning) if a non-None model_selector is passed in, so callers don't wire up a ModelSelector expecting it to work and get no feedback.


Summary: The temperature bug and missing dependency are the two items most likely to cause production issues. The error handling gap in stream() and the get_provider factory limitation are worth fixing before this lands. The test robustness issue is a quick change. Everything else is a minor polish item.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/adapters/llm/openai.py`:
- Around line 44-54: The constructor currently accepts model_selector and
complete()/stream() accept purpose, but get_model() always returns self.model,
silently ignoring model_selector and purpose; change get_model (or the
model-resolution flow used by complete/stream) to check if a model_selector is
provided and if so call model_selector.for_purpose(purpose) (falling back to
self.model if selector returns None), or if an explicit single-model override
behavior is intended, make that explicit via a flag; update references in the
OpenAI adapter methods (get_model, complete, stream) to pass the Purpose enum
value to model_selector.for_purpose and use its result for API calls so
PLANNING/EXECUTION/GENERATION routes can select different models.
- Around line 130-131: The code currently drops explicit temperature=0.0 because
it only sets kwargs["temperature"] when temperature > 0; update both complete()
and stream() to forward temperature whenever the caller provided it (i.e., check
for temperature is not None rather than > 0) and assign kwargs["temperature"] =
temperature so that a 0.0 value is sent to the OpenAI API, preserving caller
intent (modify the branches that populate the kwargs dict in complete() and
stream()).

In `@tests/adapters/test_llm_openai.py`:
- Around line 303-337: Update the two tests test_authentication_error_surfaced
and test_rate_limit_error_surfaced to assert the provider contract (that
OpenAIProvider.complete raises a ValueError with the expected message fragment)
instead of allowing either the wrapped ValueError or the raw openai exception;
call provider.complete inside a with pytest.raises(ValueError) block and assert
the exception message contains the expected fragment (e.g., "authentication" or
"rate limit") to ensure the adapter is wrapping underlying
openai.AuthenticationError and openai.RateLimitError into a ValueError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71757c9b-4b77-4bdf-97b5-70975facca8d

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca414f and e0b2ff1.

📒 Files selected for processing (3)
  • codeframe/adapters/llm/__init__.py
  • codeframe/adapters/llm/openai.py
  • tests/adapters/test_llm_openai.py

Comment on lines +44 to +54
model_selector: Optional[ModelSelector] = None,
credential_manager: Optional["CredentialManager"] = None,
):
"""Initialize the OpenAI provider.

Args:
api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
model: Default model to use for all purposes
base_url: Custom endpoint URL for OpenAI-compatible APIs
model_selector: Custom model selector (ignored — provider uses self.model)
credential_manager: Optional credential manager for secure key retrieval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't make purpose and model_selector inert.

complete() and stream() both accept purpose, and the constructor still accepts model_selector, but get_model() always returns self.model. That makes every purpose route identically and silently ignores any caller-supplied selector. If single-model mode is intentional, make it an explicit override; otherwise honor model_selector.for_purpose(purpose) at least when a selector is provided.

As per coding guidelines: codeframe/adapters/llm/**/*.py: Support Purpose enum for model selection (PLANNING, EXECUTION, GENERATION).

Also applies to: 81-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/adapters/llm/openai.py` around lines 44 - 54, The constructor
currently accepts model_selector and complete()/stream() accept purpose, but
get_model() always returns self.model, silently ignoring model_selector and
purpose; change get_model (or the model-resolution flow used by complete/stream)
to check if a model_selector is provided and if so call
model_selector.for_purpose(purpose) (falling back to self.model if selector
returns None), or if an explicit single-model override behavior is intended,
make that explicit via a flag; update references in the OpenAI adapter methods
(get_model, complete, stream) to pass the Purpose enum value to
model_selector.for_purpose and use its result for API calls so
PLANNING/EXECUTION/GENERATION routes can select different models.

Comment thread codeframe/adapters/llm/openai.py Outdated
Comment thread tests/adapters/test_llm_openai.py
- Honor explicit model_selector when provided (Purpose routing works)
- Always forward temperature=0.0 to API (OpenAI defaults to 1 when omitted)
- Tighten error tests to assert ValueError contract, not raw openai types
@claude

claude Bot commented Apr 4, 2026

Copy link
Copy Markdown

Follow-up Review

Checking the current state against the issues raised in my previous review:

Fixed:

  • Temperature is now always forwarded unconditionally in kwargs — the if temperature > 0 conditional is gone. Good.
  • Error tests now assert specifically ValueError with match strings rather than accepting raw openai.* exceptions. Good.

Still outstanding (blocking or near-blocking):

1. stream() still has no error handling

complete() wraps AuthenticationError, RateLimitError, and NotFoundError into ValueError. stream() does not — those exceptions will surface as raw openai.* types to callers that only handle ValueError. This is the same gap flagged in the previous review.

try:
    for chunk in self.client.chat.completions.create(**kwargs):
        content = chunk.choices[0].delta.content
        if content is not None:
            yield content
except openai.AuthenticationError as exc:
    raise ValueError(f"OpenAI authentication failed: {exc}") from exc
except openai.RateLimitError as exc:
    raise ValueError(f"OpenAI rate limit exceeded: {exc}") from exc
except openai.NotFoundError as exc:
    raise ValueError(f"OpenAI model not found: {exc}") from exc

2. get_provider("openai") still cannot accept base_url

elif provider_type == "openai":
    return OpenAIProvider()

The entire Ollama/vLLM use case depends on base_url, but the factory provides no way to pass it. The fix is one line — forward **kwargs:

def get_provider(provider_type: str = "anthropic", **kwargs) -> LLMProvider:
    ...
    elif provider_type == "openai":
        return OpenAIProvider(**kwargs)

3. openai still not declared as a dependency

The PR adds import openai at the top of openai.py (not lazily gated) but no pyproject.toml change is in the diff. A clean install of codeframe without openai will fail at import time as soon as from codeframe.adapters.llm import OpenAIProvider is evaluated — because __init__.py eagerly imports it. At minimum this needs an optional extra (e.g. codeframe[openai]) or openai added to core deps.


Minor (unchanged from before):

  • model_selector is silently ignored — no warning or error when a caller passes one expecting it to work.

@frankbria
frankbria merged commit c04458b into main Apr 4, 2026
10 of 11 checks passed
@frankbria
frankbria deleted the feat/543-openai-llm-adapter branch April 4, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[4.D.1] OpenAI-compatible LLM adapter (adapters/llm/openai.py)

1 participant