Skip to content

Add OpenAI-compatible chat completions endpoint#16

Open
louzt wants to merge 2 commits into
h4ks-com:mainfrom
louzt:fix/openai-chat-compat
Open

Add OpenAI-compatible chat completions endpoint#16
louzt wants to merge 2 commits into
h4ks-com:mainfrom
louzt:fix/openai-chat-compat

Conversation

@louzt
Copy link
Copy Markdown

@louzt louzt commented Apr 24, 2026

Summary

This makes the backend usable with OpenAI-compatible clients that append /chat/completions to the configured base URL.

Changes

  • add OpenAI-compatible POST /api/chat/completions and POST /v1/chat/completions
  • return an OpenAI-style chat completion payload while reusing the existing completion logic
  • replace the fragile ast.literal_eval parsing path with safe structured parsing for concatenated provider payloads
  • add an OpenAI SDK compatibility test and a parser regression test
  • disable background provider checks during pytest so the focused backend tests complete reliably
  • document the compatibility endpoints in the README

Validation

  • uv run python -m pytest tests/test_openai_compat.py -q
  • uv run python -m pytest tests/test_api.py::test_api_validation -q
  • uv run python -m pytest tests/test_chat_completions.py::TestBackwardsCompatibility -q
  • uv run python -m pytest tests/test_chat_completions.py::TestToolCalling tests/test_chat_completions.py::TestSupportsToolsField tests/test_chat_completions.py::TestPydanticModels -q

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Adds OpenAI chat-completions compatibility: new endpoints (/api/chat/completions, /v1/chat/completions), OpenAI-style request/response models and conversion, refactors completion handling, replaces fragile brace-splitting with YAML-based extraction, and adds tests plus an autouse test fixture.

Changes

Cohort / File(s) Summary
Documentation
README.md
Clarifies that OpenAI-compatible clients can call chat completions via both /v1/chat/completions and /api/chat/completions.
API Routes & Schemas
backend/routes.py
Extracts legacy completion flow into _complete_request; adds OpenAI-compatible Pydantic models and _to_openai_chat_completion converter; adds POST /api/chat/completions and POST /v1/chat/completions (rejects stream=true with 501); converts OpenAI request fields to internal CompletionRequest; registers the /v1 router.
Response Parsing
backend/adapters.py
Replaces ast-based brace-splitting with a brace-balanced candidate generator (_iter_mapping_candidates) and yaml.safe_load; centralizes extraction into _extract_content_from_payload and preserves behavior for dict/string inputs.
Dependencies
pyproject.toml
Adds runtime dependency pyyaml==6.0.2 and dev dependency openai==1.109.1.
Testing Infrastructure
tests/conftest.py
Adds autouse fixture disable_background_provider_checks() that temporarily sets settings.CHECK_WORKING_PROVIDERS = False for each test and restores it afterward; clarifies client fixture docstring.
OpenAI Compatibility Tests
tests/test_openai_compat.py
New tests: one verifies extract_openai_content handles concatenated/malformed payloads; one end-to-end test uses the OpenAI SDK against both /api/ and /v1/ compatibility prefixes with a mocked provider.

Sequence Diagram(s)

sequenceDiagram
    participant Client as OpenAI Client
    participant FastAPI as FastAPI App
    participant Provider as Chat Provider
    participant Adapter as Content Extractor
    participant Converter as Response Converter

    Client->>FastAPI: POST /v1/chat/completions (OpenAI request)
    FastAPI->>FastAPI: Map OpenAI fields -> internal CompletionRequest
    FastAPI->>Provider: _complete_request(CompletionRequest)
    Provider-->>FastAPI: Raw completion (string or dict)
    FastAPI->>Adapter: extract_openai_content(raw response)
    Adapter-->>FastAPI: Extracted message content
    FastAPI->>Converter: _to_openai_chat_completion(internal response)
    Converter-->>FastAPI: OpenAI-formatted response
    FastAPI-->>Client: Return chat.completion payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
I hopped through endpoints, two paths in sight,
Found curly braces and parsed them right,
Turned provider replies into OpenAI song,
Tests and fixtures hummed along,
A rabbit cheers compatibility bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change—adding OpenAI-compatible chat completions endpoints—which is clearly reflected in the substantial changes across routes.py, backend/adapters.py, and the new test file.
Description check ✅ Passed The description is detailed and directly related to the changeset, covering the new endpoints, parsing improvements, tests, and documentation updates that align with the file modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@louzt louzt force-pushed the fix/openai-chat-compat branch from c663ba9 to 3122a19 Compare April 24, 2026 12:50
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/test_openai_compat.py (1)

21-43: Add coverage for the /v1/chat/completions compatibility route too.

A small parameterized variant over base_url (/api/ and /v1/) would guard both public endpoints against regressions.

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

In `@tests/test_openai_compat.py` around lines 21 - 43, The test
test_openai_sdk_chat_completions_uses_compat_route only exercises the /api/
base_url; parameterize it to also test the /v1/ compatibility route by adding a
pytest.mark.parametrize over base_url values (e.g., "/api/" and "/v1/") and
feeding that into how you construct the OpenAI client (the OpenAI instantiation
and sdk_client.chat.completions.create call); ensure the assertion block remains
the same so both base_url variants call the same chat_completion dependency/mock
and validate response.object, response.model, and response.choices[0] fields for
each case.
pyproject.toml (1)

24-24: Consider pinning the OpenAI SDK dev dependency to an exact version for deterministic tests.

Using openai>=1,<2 allows version drift across minor releases, which can make test results harder to reproduce. Recent 1.x releases have been stable and follow SemVer, but exact pinning (e.g., openai==1.109.1) is a best practice for test dependencies to ensure consistent behavior across CI runs.

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

In `@pyproject.toml` at line 24, The dev dependency spec "openai>=1,<2" in
pyproject.toml allows minor-version drift; replace that entry with an exact
pinned version (for example change "openai>=1,<2" to "openai==1.109.1") in the
same pyproject.toml line, then regenerate/update the lock file (poetry lock /
pip-compile / pipenv lock as appropriate) and run the test suite to ensure
deterministic CI behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/routes.py`:
- Around line 305-307: The code currently collapses empty-string replies into
None; update the message content assignment so empty strings are preserved and
None is used only for tool-call-only messages: replace the expression
"completion_response.completion or None" used when constructing
OpenAIChatMessage with a conditional that keeps "" as-is and only returns None
when completion_response.completion is exactly None and
completion_response.tool_calls is non-empty (e.g., content =
completion_response.completion if completion_response.completion is not None
else (None if completion_response.tool_calls else "")); target the
OpenAIChatMessage construction where message=... and the symbols
completion_response.completion, completion_response.tool_calls, and
OpenAIChatMessage.

In `@tests/test_openai_compat.py`:
- Around line 44-45: Don't call app.dependency_overrides.clear() in test
cleanup; instead remove only the specific override you set earlier to avoid
affecting other tests. Locate where you set
app.dependency_overrides[<override_key>] (the overridden dependency name used in
this test, e.g., get_openai_client or similar), and replace the clear() call
with targeted removal such as deleting or popping that single key from
app.dependency_overrides so only the test's override is removed.

---

Nitpick comments:
In `@pyproject.toml`:
- Line 24: The dev dependency spec "openai>=1,<2" in pyproject.toml allows
minor-version drift; replace that entry with an exact pinned version (for
example change "openai>=1,<2" to "openai==1.109.1") in the same pyproject.toml
line, then regenerate/update the lock file (poetry lock / pip-compile / pipenv
lock as appropriate) and run the test suite to ensure deterministic CI behavior.

In `@tests/test_openai_compat.py`:
- Around line 21-43: The test test_openai_sdk_chat_completions_uses_compat_route
only exercises the /api/ base_url; parameterize it to also test the /v1/
compatibility route by adding a pytest.mark.parametrize over base_url values
(e.g., "/api/" and "/v1/") and feeding that into how you construct the OpenAI
client (the OpenAI instantiation and sdk_client.chat.completions.create call);
ensure the assertion block remains the same so both base_url variants call the
same chat_completion dependency/mock and validate response.object,
response.model, and response.choices[0] fields for each case.
🪄 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 Plus

Run ID: b1159eeb-0bad-48e0-865d-e0e3497d49fe

📥 Commits

Reviewing files that changed from the base of the PR and between daf9beb and c663ba9.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • README.md
  • backend/adapters.py
  • backend/routes.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_openai_compat.py

Comment thread backend/routes.py
Comment thread tests/test_openai_compat.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
backend/routes.py (1)

305-307: ⚠️ Potential issue | 🟠 Major

Preserve empty-string content; don’t coerce it to null at Line 306.

completion_response.completion or None converts "" to None, which breaks OpenAI chat compatibility for non-tool replies with empty text.

Proposed fix
                 message=OpenAIChatMessage(
-                    content=completion_response.completion or None,
+                    content=(
+                        None
+                        if completion_response.tool_calls
+                        and completion_response.completion == ""
+                        else completion_response.completion
+                    ),
                     tool_calls=completion_response.tool_calls,
                 ),
In the OpenAI Chat Completions response schema, should `choices[].message.content` remain `""` for normal assistant messages with empty text, and only be `null` for tool-call-only assistant messages?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routes.py` around lines 305 - 307, The current code coerces empty
assistant text to None by using completion_response.completion or None; instead
preserve an empty string for normal assistant replies and only set content to
None when the assistant produced no text and is returning tool-only output.
Update the OpenAIChatMessage construction (the content field) to assign the raw
completion_response.completion (allowing ""), and only set None when
completion_response.completion is exactly "" (or falsy) AND
completion_response.tool_calls is non-empty (i.e., tool-only responses); leave
content as the string otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/routes.py`:
- Around line 305-307: The current code coerces empty assistant text to None by
using completion_response.completion or None; instead preserve an empty string
for normal assistant replies and only set content to None when the assistant
produced no text and is returning tool-only output. Update the OpenAIChatMessage
construction (the content field) to assign the raw
completion_response.completion (allowing ""), and only set None when
completion_response.completion is exactly "" (or falsy) AND
completion_response.tool_calls is non-empty (i.e., tool-only responses); leave
content as the string otherwise.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f9155134-0af3-41d1-8784-ce2a55c3e0ba

📥 Commits

Reviewing files that changed from the base of the PR and between c663ba9 and 3122a19.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • README.md
  • backend/adapters.py
  • backend/routes.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_openai_compat.py
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/conftest.py
  • tests/test_openai_compat.py
  • backend/adapters.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/test_openai_compat.py (1)

29-29: Consider adding a guard for empty model list.

Accessing provider_and_models.all_model_names[0] will raise IndexError if no models are available. While unlikely in practice, a skip marker or explicit check would make the failure mode clearer.

Proposed improvement
+@pytest.mark.parametrize("base_path", ["/api/", "/v1/"])
+def test_openai_sdk_chat_completions_uses_compat_route(base_path: str) -> None:
+    """Verify both compatibility routes work with the OpenAI Python SDK."""
+    if not provider_and_models.all_model_names:
+        pytest.skip("No models available for testing")
+    model = provider_and_models.all_model_names[0]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_openai_compat.py` at line 29, The test directly indexes
provider_and_models.all_model_names[0], which will raise IndexError if the list
is empty; add a guard before that access (e.g., if not
provider_and_models.all_model_names: pytest.skip("no models available for
provider") or raise a clear assertion) and then assign model =
provider_and_models.all_model_names[0]; reference the symbol
provider_and_models.all_model_names and the variable model to locate where to
add this check.
backend/routes.py (1)

516-539: Consider adding Query() annotation for the provider parameter.

The provider parameter works as a query parameter but lacks OpenAPI documentation. Adding an explicit Query() annotation would provide description metadata for API consumers.

Proposed enhancement
+from fastapi import Query
+
 `@router_api.post`("/chat/completions")
 `@router_v1.post`("/chat/completions")
 async def post_openai_chat_completion(
     completion: OpenAIChatCompletionRequest,
-    provider: str | None = None,
+    provider: str | None = Query(
+        None,
+        description="Provider to use for completion. If not specified, the best available provider will be used.",
+    ),
     chat: type[g4f.ChatCompletion] = Depends(chat_completion),
 ) -> OpenAIChatCompletionResponse:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routes.py` around lines 516 - 539, The provider query parameter in
post_openai_chat_completion is currently unannotated and won't appear in OpenAPI
docs; update the function signature to use FastAPI's Query for provider (e.g.,
provider: str | None = Query(None, description="Selects the completion
provider")) so OpenAPI shows it and you can add a helpful description; locate
post_openai_chat_completion and replace the plain provider default with
Query(...) while leaving completion and chat (Depends(chat_completion))
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/routes.py`:
- Around line 516-539: The provider query parameter in
post_openai_chat_completion is currently unannotated and won't appear in OpenAPI
docs; update the function signature to use FastAPI's Query for provider (e.g.,
provider: str | None = Query(None, description="Selects the completion
provider")) so OpenAPI shows it and you can add a helpful description; locate
post_openai_chat_completion and replace the plain provider default with
Query(...) while leaving completion and chat (Depends(chat_completion))
unchanged.

In `@tests/test_openai_compat.py`:
- Line 29: The test directly indexes provider_and_models.all_model_names[0],
which will raise IndexError if the list is empty; add a guard before that access
(e.g., if not provider_and_models.all_model_names: pytest.skip("no models
available for provider") or raise a clear assertion) and then assign model =
provider_and_models.all_model_names[0]; reference the symbol
provider_and_models.all_model_names and the variable model to locate where to
add this check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 29e463ed-eb49-4bc7-bd2d-2cb7262eb8fd

📥 Commits

Reviewing files that changed from the base of the PR and between 3122a19 and 1e1ea1b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • backend/adapters.py
  • backend/routes.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_openai_compat.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

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.

1 participant