Skip to content

fix(vanilla_python): add Chat Completions API fallback - #253

Merged
andrewdonheiser merged 7 commits into
mainfrom
fix/vanilla-python-responses-api
Jul 14, 2026
Merged

andrewdonheiser merged 7 commits into
mainfrom
fix/vanilla-python-responses-api

Conversation

@andrewdonheiser

@andrewdonheiser andrewdonheiser commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add automatic fallback from Responses API to Chat Completions API when the upstream model server (e.g. vLLM) returns HTTP 400/404
  • Cache API detection result so subsequent calls skip the probe
  • Handle both response formats in _get_output_text_from_response()
  • Fix: Always fall back on 400/404, even if the Responses API previously succeeded (vLLM partially supports Responses API — simple requests succeed but multi-turn conversations fail)

The agent uses ReAct-style text patterns for tool calling (not structured API tool calling), so the fallback only needs to handle message/response format differences. The agentic loop (query()) required zero changes.

Jira

  • RHAIENG-6148
  • Part of RHAIENG-6147 (Tune behavioral test suites for CI readiness)
  • Tracking: RHAIENG-6227

Test plan

  • Deploy agent with vLLM backend (no Responses API support)
  • Run behavioral tests — 24/27 pass (previously 23/24 failed)
  • Verify tool calling works end-to-end via text patterns
  • Check logs show fallback to Chat Completions API on first call

Test results (behavioral tests against vLLM on OpenShift)

Cluster: agen-e2e-rhoai2 (ROSA HCP), namespace: adonheis-testing
Model: qwen2-5-7b-instruct via vLLM

Suite Passed Failed Total
Cost/Latency 2 0 2
Reliability 2 1 3
Response Quality 12 0 12
Tool Usage 8 2 10
Total 24 3 27

Failures (pre-existing, not regressions)

  1. test_pass_at_k_multi_tool / test_multi_tool_selection["Which brand offers the best value"] — Model flakiness: sometimes calls only search_price and skips search_reviews on comparison queries. Not related to API fallback.
  2. test_tool_not_called_for_greeting — Agent crashes with Unknown action: None when model responds to "Hello" — the ReAct regex matches Action: None in the output. Pre-existing agent bug in action parsing.

🤖 Generated with Claude Code

…ses servers

The vanilla_python agent exclusively used the OpenAI Responses API, which
causes HTTP 400 errors when the upstream model server (e.g., vLLM) does not
implement that endpoint. This adds automatic detection and fallback to the
Chat Completions API on the first call, caching the result so subsequent
calls skip the probe. No changes needed to the agentic loop since tool
calling uses text-pattern matching (ReAct style) in both API modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@andrewdonheiser, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: dc24a147-01c5-4f05-82d6-41ddab6a944e

📥 Commits

Reviewing files that changed from the base of the PR and between 7a4cd2c and 1adc872.

📒 Files selected for processing (2)
  • agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py
  • agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py
📝 Walkthrough

Walkthrough

The agent now extracts text from both OpenAI response formats and adaptively selects the Responses API or Chat Completions, caching successful capability checks and falling back on HTTP 400/404 errors. Tests cover extraction, fallback, error propagation, and cached selection.

Changes

OpenAI API adaptation

Layer / File(s) Summary
Dual response text extraction
agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py, agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py
Output extraction supports Chat Completions content, Responses API output blocks, and empty responses.
Adaptive LLM creation and fallback
agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py, agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py
LLM creation probes Responses API support, caches the result, falls back on HTTP 400/404, propagates other errors, and directly uses Chat Completions after a cached fallback decision.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AIAgent
  participant OpenAIClient
  participant ResponsesAPI
  participant ChatCompletionsAPI
  AIAgent->>OpenAIClient: call responses.create
  OpenAIClient->>ResponsesAPI: submit request
  ResponsesAPI-->>AIAgent: response or APIStatusError
  AIAgent->>ChatCompletionsAPI: fallback on HTTP 400/404
  ChatCompletionsAPI-->>AIAgent: chat completion result
Loading

Suggested reviewers: kami619, tarun-etikala, mpk-droid

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly summarizes the main change: adding a Chat Completions API fallback for the vanilla_python agent.
Description check ✅ Passed The description directly matches the changeset, covering fallback behavior, caching, response parsing, and the unchanged query flow.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vanilla-python-responses-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@andrewdonheiser andrewdonheiser changed the title fix(btest): add Chat Completions API fallback for vanilla_python agent (RHAIENG-6148) fix(vanilla_python): add Chat Completions API fallback Jul 13, 2026
andrewdonheiser and others added 2 commits July 13, 2026 17:49
vLLM partially supports the Responses API — simple single-turn requests
succeed, but multi-turn conversations fail with HTTP 400. The previous
guard `self._use_responses_api is True` prevented fallback when the API
had previously succeeded, causing the 400 to propagate as an error.

Remove the guard so any 400/404 from the Responses API permanently
switches to Chat Completions, regardless of prior success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…llback tests

Rename _responses_create to _llm_create to reflect its dual-API role, fix
ruff format issue, update stale _execute docstring, and add unit tests
covering the Responses→Chat Completions fallback (400/404), cache behavior,
and response parsing for both API formats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/m and removed size/s labels Jul 13, 2026
@andrewdonheiser
andrewdonheiser marked this pull request as ready for review July 13, 2026 22:09
@andrewdonheiser
andrewdonheiser requested a review from a team as a code owner July 13, 2026 22:09

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py (1)

286-311: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid caching the fallback on 400

A 400 from responses.create can mean a malformed request or invalid payload, not just an unsupported endpoint. Setting _use_responses_api = False on any 400 permanently downgrades later turns for the rest of the agent’s lifetime. Restrict the cached fallback to clear unsupported-endpoint failures like 404, and treat 400 as a per-call fallback without flipping the cache.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py`
around lines 286 - 311, Update the API error handling in _llm_create so only a
404 sets _use_responses_api = False; for a 400, fall back to
_chat_completions_create for the current call without changing the cached API
preference. Preserve re-raising all other status codes and keep the existing
fallback logging behavior appropriate to whether the downgrade is cached.
🧹 Nitpick comments (1)
agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py (1)

60-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test for the "prior success then later 400/404" transition.

The PR objective explicitly states the fallback should permanently switch "including cases where earlier requests succeeded," but no test sets _use_responses_api = True first and then verifies a subsequent 400/404 flips it to False. Current tests only cover starting from None. Add a test seeding agent._use_responses_api = True before triggering the 400/404 exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py`
around lines 60 - 93, Add coverage for the prior-success transition in the
fallback tests around _llm_create: seed agent._use_responses_api = True before
triggering an APIStatusError for both 400 and 404 cases, then assert the chat
completions fallback response is returned and _use_responses_api becomes False.
Preserve the existing assertions for the fallback call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py`:
- Around line 286-311: Update the API error handling in _llm_create so only a
404 sets _use_responses_api = False; for a 400, fall back to
_chat_completions_create for the current call without changing the cached API
preference. Preserve re-raising all other status codes and keep the existing
fallback logging behavior appropriate to whether the downgrade is cached.

---

Nitpick comments:
In `@agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py`:
- Around line 60-93: Add coverage for the prior-success transition in the
fallback tests around _llm_create: seed agent._use_responses_api = True before
triggering an APIStatusError for both 400 and 404 cases, then assert the chat
completions fallback response is returned and _use_responses_api becomes False.
Preserve the existing assertions for the fallback call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: d6d1674a-7ccc-4279-9f04-511d15f089ae

📥 Commits

Reviewing files that changed from the base of the PR and between 3c778d9 and 7a4cd2c.

📒 Files selected for processing (2)
  • agents/vanilla_python/templates/openai_responses_agent/src/openai_responses_agent/agent.py
  • agents/vanilla_python/templates/openai_responses_agent/tests/test_agent.py

@shricharan-ks shricharan-ks self-assigned this Jul 14, 2026

@shricharan-ks shricharan-ks 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.

Code Review — PR #253 (medium effort, 6 angles, verified)

This PR adds automatic fallback from the Responses API to Chat Completions API when the upstream model server (e.g. vLLM) returns HTTP 400/404. It handles both response formats in _get_output_text_from_response(), caches the API detection result per-agent-instance, and adds comprehensive unit tests. 2 files changed, 192 additions, 7 deletions.


Findings (6 survived verification, most severe first)

1. 🔴 agent.py:241 — if temp != 0 silently drops temperature=0, API defaults to 1.0 (CONFIRMED)

if temp != 0:
    kwargs["temperature"] = temp

The constructor defaults temperature=0 for deterministic output. But 0 is falsy in Python, so if temp != 0: evaluates to False — temperature is never sent to the API. Both OpenAI's Chat Completions and Responses APIs default to temperature=1.0 when omitted. Every request runs non-deterministically instead of the intended deterministic mode.

This is pre-existing in the Responses API path (line 283) but the PR propagates it to the new _chat_completions_create. Fix: if temp is not None: or always include "temperature": temp in kwargs.


2. 🟡 agent.py:195 — API detection cache resets every HTTP request (CONFIRMED)

_AIAgentAdapter.run() creates a new AIAgent per HTTP request. Since _use_responses_api is an instance variable initialized to None, the cache never persists across requests. Against a vLLM backend that doesn't support the Responses API, every request probes → fails → retries via Chat Completions, doubling first-turn latency and flooding logs with fallback warnings.

The cache works within a single query() call's multi-turn ReAct loop (2-5 turns), but not across HTTP requests. Consider lifting the detection state to _AIAgentAdapter (process-lifetime) or a module-level variable.


3. 🟡 test_agent.py — No test for the multi-turn fallback scenario the PR fixes (CONFIRMED)

The PR description says: "Fix: Always fall back on 400/404, even if the Responses API previously succeeded (vLLM partially supports Responses API — simple requests succeed but multi-turn conversations fail)."

All 5 tests exercise single, isolated _llm_create() calls. No test calls _llm_create twice where the first succeeds (→ True) and the second gets 400 and must transition to False. This is the exact scenario motivating commit d525731a and is the one most likely to regress.

Missing test: mock responses.create to succeed on call 1, raise APIStatusError(400) on call 2, verify call 2 falls back to chat.completions.create.


4. 🟡 agent.py:293 — HTTP 400 is too broad for "endpoint not supported" (PLAUSIBLE)

The except block treats any 400 as "server doesn't support Responses API" and permanently sets _use_responses_api = False. But HTTP 400 also covers legitimate request errors (too-long context, malformed content, unsupported parameter). A single bad request on a server that does support the Responses API would permanently disable it.

Consider checking the error body/message for endpoint-not-found indicators, or limiting the permanent flip to 404 only.


5. 🟡 agent.py:137 — choices[0].message accessed without guard (PLAUSIBLE)

choices = getattr(response, "choices", None)
if choices:
    return getattr(choices[0].message, "content", None) or ""

The Responses API path uses defensive getattr() throughout, but the new Chat Completions path directly accesses .message on choices[0]. An OpenAI-compatible server returning a non-standard choices schema (e.g., streaming delta instead of message) would raise an unhandled AttributeError.


6. 🟡 agent.py:233/273 — Parameter resolution duplicated across methods (PLAUSIBLE)

_chat_completions_create and _llm_create both have identical 3-line parameter resolution (msg_list = messages if..., temp = ..., model_id = ...) and the same if temp != 0 guard. Adding a parameter to one without the other would cause silent behavioral divergence between the two API paths.


What looks good

  • Fallback design: The try/except structure correctly catches 400/404 and falls back, even after a previous Responses API success — the key fix for vLLM's partial support
  • Response format handling: _get_output_text_from_response correctly dispatches between Chat Completions (choices) and Responses API (output) formats
  • Test coverage: 5 well-structured unit tests covering success caching, 400/404 fallback, error propagation, and cached fast-path
  • Zero changes to query(): The agentic loop is correctly insulated from the API abstraction change
  • Clean typing modernization: Optional[str] → str | None, Dict → dict throughout

Summary

Finding #1 (temperature=0) is the most impactful — affects every request. It's pre-existing but worth fixing in this PR since _chat_completions_create is new code. Finding #2 (cache scope) is a design improvement worth considering. Finding #3 (missing test) is easy to add and covers the exact scenario that motivated the fix.

andrewdonheiser and others added 2 commits July 14, 2026 11:39
- Fix temperature=0 bug: `if temp != 0` → `if temp is not None` so
  temperature=0 is actually sent to the API (was silently dropped,
  causing non-deterministic responses)
- Differentiate 400 vs 404 in fallback caching: only 404 permanently
  disables Responses API; 400 falls back per-call without caching
  (400 can indicate a legitimate request error, not just unsupported)
- Add tests for multi-turn fallback (prior success → later failure)
  and temperature=0 being passed through

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andrewdonheiser

Copy link
Copy Markdown
Contributor Author

Review Response — d58ca66

Thanks for the thorough reviews @shricharan-ks and CodeRabbit! Pushed a commit addressing the actionable findings.

Addressed

# Finding Fix
1 🔴 if temp != 0 silently drops temperature=0 (API defaults to 1.0) Changed to if temp is not None: in both _chat_completions_create and _llm_create
2 🟡 HTTP 400 too broad for permanent cache flip Only 404 now permanently sets _use_responses_api = False. On 400, falls back for the current call only without caching
3 🟡 No test for multi-turn fallback (success → later failure) Added test_fallback_on_400_after_prior_success, test_fallback_on_404_after_prior_success, and test_temperature_zero_is_passed

Not addressed (follow-up)

All 12 unit tests pass, lint and formatting clean.

@tarun-etikala tarun-etikala 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.

Clean PR — fallback logic is correct, the 400-vs-404 distinction is well-reasoned for vLLM's partial Responses API support, and test coverage is thorough.

Two suggestions (non-blocking):

  1. API detection cache doesn't persist across HTTP requests — _AIAgentAdapter.run() creates a new AIAgent per request, so _use_responses_api resets to None each time. On a vLLM backend, every request pays for a wasted 404 probe before falling back. Consider lifting the flag to _AIAgentAdapter and syncing it back after each query() call. The log message "permanently falling back" is also misleading since the state is lost on the next request.

  2. Minor test style — test_temperature_zero_is_passed uses call_kwargs[1]["temperature"]; call_kwargs.kwargs["temperature"] is slightly more readable.

@andrewdonheiser
andrewdonheiser merged commit ffc8532 into main Jul 14, 2026
15 checks passed
@andrewdonheiser
andrewdonheiser deleted the fix/vanilla-python-responses-api branch July 14, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants