Skip to content

feat(router): add OpenAI Responses API and Home Assistant model support (#374)#382

Open
sheepdestroyer wants to merge 1 commit into
masterfrom
feat/responses-api-ha-support
Open

feat(router): add OpenAI Responses API and Home Assistant model support (#374)#382
sheepdestroyer wants to merge 1 commit into
masterfrom
feat/responses-api-ha-support

Conversation

@sheepdestroyer

@sheepdestroyer sheepdestroyer commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

This PR addresses issue #374 by implementing OpenAI Responses API support and model aliases required by Home Assistant's openai_conversation integration.

Changes Included

  1. Responses API Endpoints:

    • Added POST /v1/responses and POST /responses handlers in router/main.py that proxy requests to LiteLLM's /v1/responses endpoint.
    • Preserves model triage classification when auto-routing models (llm-routing-auto-free) are requested.
    • Supports both streaming (SSE events) and non-streaming responses.
  2. Model Aliases:

    • Added gpt-4o-mini and gpt-4o model definitions to litellm/config.yaml routing to local-qwen-3.6-hass (thinking disabled).
    • Added local-qwen-3.6, local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o to public_model_groups so GET /v1/models lists them.
    • Included new model aliases in DIRECT_TIERS in router/main.py to bypass classifier overhead when specified directly.
  3. Tool Support:

    • Verified and enabled support for Home Assistant Assist actions (function tool type returning function_call output), code_interpreter, and web_search.
  4. Testing & Documentation:

    • Added automated pytest test suite in router/tests/test_responses_api.py covering model routing, Responses API requests, tools, streaming, and error handling (363/363 tests passing).
    • Added section 9e to README.md detailing Home Assistant configuration and capability requirements.
    • Updated system wiki (wiki/entities/llm-routing.md and wiki/log.md).

Closes #374

Summary by Sourcery

Add OpenAI Responses API proxy support and Home Assistant-oriented model aliases and documentation, including tests for the new responses endpoint and routing behavior.

New Features:

  • Expose OpenAI-compatible Responses API endpoints that proxy to LiteLLM, supporting streaming and non-streaming requests.
  • Introduce public model aliases (local-qwen-3.6, local-qwen-3.6-hass, gpt-4o-mini, gpt-4o) for use with Home Assistant and other clients.

Enhancements:

  • Extend direct tier model list to include new local Qwen and GPT-4o aliases to avoid unnecessary classification overhead.
  • Document Home Assistant integration, supported models, and Responses API/tool compatibility in the README, and update links to key project files.

Tests:

  • Add pytest coverage for the Responses API endpoint, including direct and auto-routed models, tool usage, streaming behavior, and error handling.

Summary by CodeRabbit

  • New Features

    • Added support for OpenAI Responses API requests through /v1/responses and /responses.
    • Added streaming responses, automatic model routing, tool/function calling, and Home Assistant integration support.
    • Added compatibility aliases for local Qwen and GPT-4o models.
  • Documentation

    • Expanded setup and configuration guidance for container health checks, memory services, Home Assistant, model aliases, and Responses API capabilities.
  • Bug Fixes

    • Improved handling of malformed request payloads with clear validation errors.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added documentation Improvements or additions to documentation router litellm labels Jul 25, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds an OpenAI Responses API proxy endpoint with auto-routing and streaming support, introduces Home Assistant-friendly model aliases and visibility, and documents and tests the new Responses behavior and tools integration.

Sequence diagram for OpenAI Responses API proxy with auto-routing and streaming

sequenceDiagram
    actor HomeAssistant
    participant Router as responses_api
    participant Classifier as classify_request
    participant LiteLLM as LiteLLM_v1_responses

    HomeAssistant->>Router: POST /v1/responses (model or auto model)
    Router->>Router: sync_cooldowns_from_valkey
    Router->>Router: parse last_user_message from input/instructions
    alt auto model requested
        Router->>Classifier: classify_request(last_user_message, bypass_cache)
        Classifier-->>Router: target_model
    else direct model alias
        Router->>Router: target_model = client_model
    end
    Router->>Router: body_to_send[model] = target_model
    Router->>Router: get_http_client
    alt stream == true
        Router->>LiteLLM: client.stream("POST", /v1/responses, json=body_to_send)
        LiteLLM-->>Router: SSE chunks
        Router-->>HomeAssistant: StreamingResponse(text/event-stream)
    else non-streaming
        Router->>LiteLLM: client.post(/v1/responses, json=body_to_send)
        LiteLLM-->>Router: JSON response
        Router-->>HomeAssistant: Response(status_code, headers, content)
    end
Loading

File-Level Changes

Change Details Files
Implement OpenAI-compatible Responses API proxy endpoint with auto-model triage and streaming/non-streaming support.
  • Add POST /v1/responses and /responses FastAPI route that proxies to LiteLLM /v1/responses.
  • Parse input/instructions to extract user text for classification when auto-routing models are requested.
  • Invoke classify_request for auto models, preserve chosen target_model, and forward updated payload to LiteLLM.
  • Handle streaming via client.stream and return StreamingResponse with SSE bytes; handle non-streaming via client.post and Response.
  • Synchronize cooldowns from Valkey and normalize Authorization header using LITELLM_MASTER_KEY when necessary.
  • Strip hop-by-hop headers on proxied non-streaming responses and surface HTTP 400/502 errors for invalid JSON or proxy failure.
router/main.py
Expose Home Assistant model aliases and local Qwen variants through LiteLLM configuration and direct tiers.
  • Add local-qwen-3.6, local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o to DIRECT_TIERS to bypass classifier when directly requested.
  • Register new model groups in litellm/config.yaml with openai/local-qwen-3.6-hass backend, disabled thinking, and detailed model_info metadata.
  • Mark these models as public model groups and extend public_model_groups list for discovery via GET /v1/models.
router/main.py
litellm/config.yaml
Document Home Assistant integration and Responses API/tool compatibility and fix README link targets.
  • Add README section describing Home Assistant openai_conversation setup, supported models, and Responses API/tool support.
  • Update existing README references to internal files to use full GitHub URLs for quadlets, pod.yaml, memory_mcp.py, and verification script.
README.md
Add pytest coverage for Responses API endpoint behavior, including triage, tools, streaming, and error handling.
  • Create router/tests/test_responses_api.py with tests for direct model non-streaming proxying and response shape.
  • Add test verifying classifier is invoked for llm-routing-auto-free and that the forwarded payload uses the classified target model.
  • Add test validating tool payloads (function tools, code_interpreter, web_search) and function_call outputs.
  • Add tests for invalid JSON payload raising HTTP 400 and for stream=True returning StreamingResponse with event-stream media type.
router/tests/test_responses_api.py

Assessment against linked issues

Issue Objective Addressed Explanation
#374 Implement an OpenAI-compatible /v1/responses endpoint that proxies to LiteLLM, supports Home Assistant-style tools (function calls, code_interpreter, web_search), streaming/non-streaming behavior, and maintains existing routing/classifier behavior without breaking /v1/chat/completions.
#374 Expose Home Assistant-usable models via /v1/models by adding appropriate model aliases (e.g., gpt-4o-mini, gpt-4o, local-qwen-3.6, local-qwen-3.6-hass) and ensuring they route to the intended backend models with consistent authentication and alias behavior.
#374 Add automated tests and documentation for the Responses API behavior, including model alias visibility, tool handling, streaming/non-streaming responses, and error handling/capability description for unsupported tools or malformed requests, specifically in the Home Assistant integration context.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds OpenAI Responses API proxy endpoints, Home Assistant-compatible model aliases, direct routing entries, automated coverage for streaming and tool calls, and README documentation for configuration and compatibility.

Changes

Responses API and Home Assistant integration

Layer / File(s) Summary
Home Assistant model aliases
litellm/config.yaml
Adds public Qwen and GPT-4o aliases with capability metadata backed by the local Home Assistant model.
Responses API proxy flow
router/main.py
Adds /v1/responses and /responses, automatic model classification, streaming support, error forwarding, and direct routing for new aliases.
Validation and documentation
router/tests/test_responses_api.py, README.md
Tests direct, automatic, tool, invalid-payload, and streaming requests, and documents Home Assistant Responses API configuration and compatibility.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant Classifier
  participant LiteLLM
  Client->>Router: POST /v1/responses
  Router->>Classifier: Classify auto-routed request
  Classifier-->>Router: Selected target model
  Router->>LiteLLM: Proxy Responses API request
  LiteLLM-->>Router: JSON response or SSE stream
  Router-->>Client: Forward response
Loading

Possibly related PRs

Suggested labels: tests

🚥 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 clearly matches the main change: Responses API support and Home Assistant model aliases.
Linked Issues check ✅ Passed The changes align with #374 by adding Responses API routes, model aliases, tool handling, streaming, tests, and documentation.
Out of Scope Changes check ✅ Passed The modified README, router, config, and tests all support the stated Responses API and Home Assistant compatibility work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/responses-api-ha-support

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.

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

Hey - I've found 1 issue, and left some high level feedback:

  • The triage and proxy logic in responses_api largely duplicates the behavior in chat_completions; consider extracting a shared helper for model selection, auth header construction, and proxying to LiteLLM to keep these endpoints consistent as behavior evolves.
  • The AUTO_MODELS set is now defined separately in both responses_api and chat_completions; centralizing this configuration (e.g., as a module-level constant) would reduce the chance of model list drift between endpoints.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The triage and proxy logic in `responses_api` largely duplicates the behavior in `chat_completions`; consider extracting a shared helper for model selection, auth header construction, and proxying to LiteLLM to keep these endpoints consistent as behavior evolves.
- The `AUTO_MODELS` set is now defined separately in both `responses_api` and `chat_completions`; centralizing this configuration (e.g., as a module-level constant) would reduce the chance of model list drift between endpoints.

## Individual Comments

### Comment 1
<location path="router/main.py" line_range="1999-2024" />
<code_context>
+    input_field = body.get("input")
+    if isinstance(input_field, str):
+        last_user_message = input_field
+    elif isinstance(input_field, list):
+        for item in input_field:
+            if isinstance(item, str):
+                last_user_message += item
+            elif isinstance(item, dict):
+                if item.get("type") == "text":
+                    last_user_message += item.get("text", "")
+                elif item.get("role") == "user":
+                    content = item.get("content") or ""
+                    if isinstance(content, str):
+                        last_user_message += content
+                    elif isinstance(content, list):
+                        last_user_message += "".join(
+                            b.get("text", "")
+                            for b in content
+                            if isinstance(b, dict) and b.get("type") == "text"
+                        )
+
+    if not last_user_message:
</code_context>
<issue_to_address>
**suggestion:** Concatenating multiple input segments without separators may degrade triage classification quality.

When `input` is a list, all segments are concatenated into `last_user_message` without any separators. For long or structured inputs this can obscure boundaries between messages or content types and negatively impact `classify_request`. Consider inserting simple delimiters (spaces/newlines) between segments or restricting which parts are included in triage to improve routing accuracy.

```suggestion
    last_user_message = ""
    input_field = body.get("input")
    if isinstance(input_field, str):
        last_user_message = input_field
    elif isinstance(input_field, list):
        # Collect segments and join with a delimiter to preserve boundaries for triage
        segments: list[str] = []
        for item in input_field:
            if isinstance(item, str):
                segments.append(item)
            elif isinstance(item, dict):
                if item.get("type") == "text":
                    segments.append(item.get("text", ""))
                elif item.get("role") == "user":
                    content = item.get("content") or ""
                    if isinstance(content, str):
                        segments.append(content)
                    elif isinstance(content, list):
                        segments.append(
                            "".join(
                                b.get("text", "")
                                for b in content
                                if isinstance(b, dict) and b.get("type") == "text"
                            )
                        )
        # Use a simple space delimiter to avoid degrading classification by merging segments
        last_user_message = " ".join(s for s in segments if s)

    if not last_user_message:
        instructions = body.get("instructions")
        if isinstance(instructions, str):
            last_user_message = instructions
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread router/main.py
Comment on lines +1999 to +2024
last_user_message = ""
input_field = body.get("input")
if isinstance(input_field, str):
last_user_message = input_field
elif isinstance(input_field, list):
for item in input_field:
if isinstance(item, str):
last_user_message += item
elif isinstance(item, dict):
if item.get("type") == "text":
last_user_message += item.get("text", "")
elif item.get("role") == "user":
content = item.get("content") or ""
if isinstance(content, str):
last_user_message += content
elif isinstance(content, list):
last_user_message += "".join(
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
)

if not last_user_message:
instructions = body.get("instructions")
if isinstance(instructions, str):
last_user_message = instructions

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.

suggestion: Concatenating multiple input segments without separators may degrade triage classification quality.

When input is a list, all segments are concatenated into last_user_message without any separators. For long or structured inputs this can obscure boundaries between messages or content types and negatively impact classify_request. Consider inserting simple delimiters (spaces/newlines) between segments or restricting which parts are included in triage to improve routing accuracy.

Suggested change
last_user_message = ""
input_field = body.get("input")
if isinstance(input_field, str):
last_user_message = input_field
elif isinstance(input_field, list):
for item in input_field:
if isinstance(item, str):
last_user_message += item
elif isinstance(item, dict):
if item.get("type") == "text":
last_user_message += item.get("text", "")
elif item.get("role") == "user":
content = item.get("content") or ""
if isinstance(content, str):
last_user_message += content
elif isinstance(content, list):
last_user_message += "".join(
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
)
if not last_user_message:
instructions = body.get("instructions")
if isinstance(instructions, str):
last_user_message = instructions
last_user_message = ""
input_field = body.get("input")
if isinstance(input_field, str):
last_user_message = input_field
elif isinstance(input_field, list):
# Collect segments and join with a delimiter to preserve boundaries for triage
segments: list[str] = []
for item in input_field:
if isinstance(item, str):
segments.append(item)
elif isinstance(item, dict):
if item.get("type") == "text":
segments.append(item.get("text", ""))
elif item.get("role") == "user":
content = item.get("content") or ""
if isinstance(content, str):
segments.append(content)
elif isinstance(content, list):
segments.append(
"".join(
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
)
)
# Use a simple space delimiter to avoid degrading classification by merging segments
last_user_message = " ".join(s for s in segments if s)
if not last_user_message:
instructions = body.get("instructions")
if isinstance(instructions, str):
last_user_message = instructions

@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

🧹 Nitpick comments (2)
litellm/config.yaml (1)

124-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Three near-identical litellm_params/model_info blocks — consider YAML anchors.

local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o repeat byte-identical litellm_params and model_info blocks. YAML anchors/merge keys (&anchor/<<: *anchor) would remove the duplication and reduce drift risk if one copy is updated but the others are forgotten (as likely happened with the supports_reasoning flag above).

♻️ Example using YAML anchors
- litellm_params: &hass_params
    api_base: LLAMA_CLASSIFIER_URL_PLACEHOLDER
    api_key: local-token
    model: openai/local-qwen-3.6-hass
    extra_body:
      chat_template_kwargs:
        enable_thinking: false
    request_timeout: 600
  model_name: local-qwen-3.6-hass
  model_info: &hass_model_info
    supports_vision: true
    supports_reasoning: false
    supports_function_calling: true
    mode: chat
    max_tokens: 524288
    max_input_tokens: 524288
    is_public_model_group: true
- litellm_params: *hass_params
  model_name: gpt-4o-mini
  model_info: *hass_model_info
- litellm_params: *hass_params
  model_name: gpt-4o
  model_info: *hass_model_info
🤖 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 `@litellm/config.yaml` around lines 124 - 165, Replace the duplicated
litellm_params and model_info mappings for local-qwen-3.6-hass, gpt-4o-mini, and
gpt-4o with YAML anchors and aliases. Define each shared mapping once, then
reference it from the three model entries while preserving their distinct
model_name values and the intended supports_reasoning setting.
router/tests/test_responses_api.py (1)

1-177: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for structured input arrays and streaming error paths.

All five tests use a plain string input; none exercise the list-based input shape ([{"role": "user", "content": [{"type": "input_text", ...}]}]) that real Responses API clients (including the OpenAI SDK) send for multi-part/multi-turn requests. That gap is exactly why the type == "text" vs input_text bug in responses_api's extraction logic (see router/main.py review) went untested. Likewise, test_responses_api_streaming only simulates a successful stream — there's no test asserting behavior when the upstream stream returns a non-200 status, which is why the missing status-code check in the streaming branch wasn't caught.

Consider adding:

  • A test with "input": [{"role": "user", "content": [{"type": "input_text", "text": "..."}]}] asserting the classifier receives the extracted text.
  • A test where mock_client.stream/upstream returns a non-200 status during a streaming request, asserting an appropriate error is surfaced rather than a 200.
🤖 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 `@router/tests/test_responses_api.py` around lines 1 - 177, Extend the response
API tests with structured input coverage: add a list-based input containing
input_text content and assert responses_api passes the extracted text to
classify_request. Add a streaming failure test using a non-200 upstream response
from mock_client.stream, asserting responses_api surfaces an appropriate error
response instead of returning a successful StreamingResponse.
🤖 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.

Inline comments:
In `@litellm/config.yaml`:
- Around line 124-165: Update the model_info entries for the
local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o aliases to set supports_reasoning
to false. Leave the existing enable_thinking configuration and other capability
flags unchanged, matching the local-qwen-3.6 behavior.

In `@router/main.py`:
- Around line 2050-2072: Update the streaming branch in responses_api’s
response_streamer to validate the upstream resp.status_code before yielding any
bytes. Propagate non-success responses as HTTP errors, preserving the existing
successful StreamingResponse behavior and matching the status handling used by
chat_completions.
- Around line 1999-2020: Update the last-user-input extraction in responses_api
to recognize input_text for both top-level items and nested content blocks, and
change the input-list traversal to inspect items in reverse and stop after
extracting the latest user turn. Keep chat_completions aligned with this
behavior by selecting only the most recent user message rather than
concatenating multiple turns.

---

Nitpick comments:
In `@litellm/config.yaml`:
- Around line 124-165: Replace the duplicated litellm_params and model_info
mappings for local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o with YAML anchors and
aliases. Define each shared mapping once, then reference it from the three model
entries while preserving their distinct model_name values and the intended
supports_reasoning setting.

In `@router/tests/test_responses_api.py`:
- Around line 1-177: Extend the response API tests with structured input
coverage: add a list-based input containing input_text content and assert
responses_api passes the extracted text to classify_request. Add a streaming
failure test using a non-200 upstream response from mock_client.stream,
asserting responses_api surfaces an appropriate error response instead of
returning a successful StreamingResponse.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e431313-2d14-4759-a6b2-c78600166af2

📥 Commits

Reviewing files that changed from the base of the PR and between dcdd018 and 5a3e7e7.

📒 Files selected for processing (4)
  • README.md
  • litellm/config.yaml
  • router/main.py
  • router/tests/test_responses_api.py

Comment thread litellm/config.yaml
Comment on lines +124 to +165
model_info:
supports_vision: true
supports_reasoning: true
supports_function_calling: true
mode: chat
max_tokens: 524288
max_input_tokens: 524288
is_public_model_group: true
- litellm_params:
api_base: LLAMA_CLASSIFIER_URL_PLACEHOLDER
api_key: local-token
model: openai/local-qwen-3.6-hass
extra_body:
chat_template_kwargs:
enable_thinking: false
request_timeout: 600
model_name: gpt-4o-mini
model_info:
supports_vision: true
supports_reasoning: true
supports_function_calling: true
mode: chat
max_tokens: 524288
max_input_tokens: 524288
is_public_model_group: true
- litellm_params:
api_base: LLAMA_CLASSIFIER_URL_PLACEHOLDER
api_key: local-token
model: openai/local-qwen-3.6-hass
extra_body:
chat_template_kwargs:
enable_thinking: false
request_timeout: 600
model_name: gpt-4o
model_info:
supports_vision: true
supports_reasoning: true
supports_function_calling: true
mode: chat
max_tokens: 524288
max_input_tokens: 524288
is_public_model_group: true

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does LiteLLM's model_info supports_reasoning flag change request/response handling when true vs false for OpenAI-compatible backends?

💡 Result:

In LiteLLM, the supports_reasoning flag (found in model_info and accessed via litellm.supports_reasoning(model)) is primarily a capability indicator used by LiteLLM’s internal logic to determine if a model supports reasoning-related parameters (such as thinking blocks or reasoning effort) [1][2]. It does not automatically change request/response handling solely based on its true/false value; rather, it is a metadata lookup that informs whether LiteLLM permits or attempts to translate specific reasoning parameters for that model [1][3][4]. When using OpenAI-compatible backends, the presence and state of this flag influence behavior in the following ways: 1. Parameter Validation: LiteLLM uses this flag to validate requests [5][4]. If you attempt to pass reasoning-specific parameters (like include_reasoning or reasoning_effort) to a model where supports_reasoning is false, LiteLLM may raise an error (e.g., UnsupportedParamsError) or fail to process the request correctly because the model is not registered as supporting those features [3][5]. 2. Parameter Transformation: When set to true, LiteLLM’s internal logic is more likely to permit and correctly transform or pass through reasoning-related fields (like thinking blocks) to the backend provider [1][4]. If the flag is false, LiteLLM may strip these parameters or fail to map them, depending on the current configuration and the use of parameters like drop_params [1][5]. 3. Automated Bridging: For some providers, when a model with supports_reasoning=true is used in conjunction with specific tools or parameters, LiteLLM may automatically route the request through internal bridges (such as the Responses API bridge) to ensure compatibility [1][6]. In summary, the flag acts as a gatekeeper for reasoning features within the LiteLLM library [1][2]. Setting it to true tells LiteLLM that it is valid to attempt reasoning-based interactions with that specific model, whereas setting it to false restricts those capabilities to prevent invalid parameter errors when communicating with backends that do not support them [3][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== config excerpt =="
sed -n '100,180p' litellm/config.yaml

echo
echo "== model aliases/references =="
rg -n "local-qwen-3.6-hass|enable_thinking|supports_reasoning|gpt-4o-mini|gpt-4o|model_group" litellm -S

echo
echo "== repository files =="
git ls-files litellm | sed -n '1,120p'

Repository: sheepdestroyer/LLM-Routing

Length of output: 5429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== entrypoint outline =="
ast-grep outline litellm/entrypoint.py || true

echo
echo "== entrypoint relevant sections =="
wc -l litellm/entrypoint.py
sed -n '1,260p' litellm/entrypoint.py

echo
echo "== tests =="
cat -n litellm/tests/test_entrypoint.py

Repository: sheepdestroyer/LLM-Routing

Length of output: 10311


Set supports_reasoning to false for the hass-only aliases.

local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o use chat_template_kwargs.enable_thinking: false, but their model_info still declares reasoning support. Sync these aliases with the local-qwen-3.6 behavior so model capability and configuration match.

🤖 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 `@litellm/config.yaml` around lines 124 - 165, Update the model_info entries
for the local-qwen-3.6-hass, gpt-4o-mini, and gpt-4o aliases to set
supports_reasoning to false. Leave the existing enable_thinking configuration
and other capability flags unchanged, matching the local-qwen-3.6 behavior.

Comment thread router/main.py
Comment on lines +1999 to +2020
last_user_message = ""
input_field = body.get("input")
if isinstance(input_field, str):
last_user_message = input_field
elif isinstance(input_field, list):
for item in input_field:
if isinstance(item, str):
last_user_message += item
elif isinstance(item, dict):
if item.get("type") == "text":
last_user_message += item.get("text", "")
elif item.get("role") == "user":
content = item.get("content") or ""
if isinstance(content, str):
last_user_message += content
elif isinstance(content, list):
last_user_message += "".join(
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

OpenAI Responses API input array content part type input_text vs text field name

💡 Result:

The distinction between input_text and text field names in the OpenAI API depends on which specific API endpoint and resource you are using. These are not interchangeable; using the incorrect type often results in an invalid_value error [1][2]. 1. Chat Completions API: For the standard Chat Completions endpoint (client.chat.completions.create), the content part type for text is strictly "text" [3]. Example structure: { "role": "user", "content": [ { "type": "text", "text": "Your message here" } ] } 2. Responses API: For the newer Responses API (client.responses.create), the content part type for text is "input_text" [4][5][6]. Example structure: { "role": "user", "content": [ { "type": "input_text", "text": "Your message here" } ] } Note that within the Responses API, if you are providing context from a previous assistant response, you may be required to use "output_text" instead of "input_text" [7][2]. In summary, always check the documentation for the specific endpoint you are targeting, as the API enforces these type schemas strictly to distinguish between input and output modalities across different services [1][7][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file:"
git ls-files | rg '(^|/)router/main\.py$' || true

echo
echo "Relevant section:"
sed -n '1960,2045p' router/main.py

echo
echo "Search for last_user_message and chat_completions handling:"
rg -n "last_user_message|chat_completions|input_field|AUTO_MODELS|input_text|type.*text" router/main.py | head -200

echo
echo "Diff stat/name-only:"
git diff --stat
git diff -- router/main.py | sed -n '1,220p'

Repository: sheepdestroyer/LLM-Routing

Length of output: 5467


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant chat_completions section:"
sed -n '2100,2245p' router/main.py

echo
echo "Programmatic behavior probe for current extraction loop:"
python3 - <<'PY'
def extract_current(input_field):
    last_user_message = ""
    if isinstance(input_field, str):
        last_user_message = input_field
    elif isinstance(input_field, list):
        for item in input_field:
            if isinstance(item, str):
                last_user_message += item
            elif isinstance(item, dict):
                if item.get("type") == "text":
                    last_user_message += item.get("text", "")
                elif item.get("role") == "user":
                    content = item.get("content") or ""
                    if isinstance(content, str):
                        last_user_message += content
                    elif isinstance(content, list):
                        last_user_message += "".join(
                            b.get("text", "")
                            for b in content
                            if isinstance(b, dict) and b.get("type") == "text"
                        )
    return last_user_message

def extract_proposed(input_field):
    last_user_message = ""
    if isinstance(input_field, str):
        last_user_message = input_field
    elif isinstance(input_field, list):
        for item in reversed(input_field):
            if isinstance(item, dict) and item.get("role") == "user":
                content = item.get("content") or ""
                if isinstance(content, str):
                    last_user_message = content
                elif isinstance(content, list):
                    last_user_message = "".join(
                        b.get("text", "")
                        for b in content
                        if isinstance(b, dict) and b.get("type") == "input_text"
                    )
                break
    return last_user_message

payloads = [
    {"Responses structured input with input_text": [
        {"role": "user", "content": [{"type": "input_text", "text": "Last user input"}]},
        {"role": "assistant", "content": "assistant output"},
        {"role": "user", "content": [{"type": "input_text", "text": "Oldest user input"}]},
    ]},
    {"Responses with top-level text object": [
        {"role": "user", "content": [{"type": "text", "text": "User text"}]},
    ]},
    {"Responses plain strings": [
        "Last",
        "Oldest",
    ]},
]
for name, input_field in payloads.items():
    current = extract_current(input_field)
    proposed = extract_proposed(input_field)
    print(f"{name}: current={current!r}, proposed={proposed!r}")
PY

Repository: sheepdestroyer/LLM-Routing

Length of output: 5610


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Programmatic behavior probe for current extraction loop:"
python3 - <<'PY'
def extract_current(input_field):
    last_user_message = ""
    if isinstance(input_field, str):
        last_user_message = input_field
    elif isinstance(input_field, list):
        for item in input_field:
            if isinstance(item, str):
                last_user_message += item
            elif isinstance(item, dict):
                if item.get("type") == "text":
                    last_user_message += item.get("text", "")
                elif item.get("role") == "user":
                    content = item.get("content") or ""
                    if isinstance(content, str):
                        last_user_message += content
                    elif isinstance(content, list):
                        last_user_message += "".join(
                            b.get("text", "")
                            for b in content
                            if isinstance(b, dict) and b.get("type") == "text"
                        )
    return last_user_message

def extract_proposed(input_field):
    last_user_message = ""
    if isinstance(input_field, str):
        last_user_message = input_field
    elif isinstance(input_field, list):
        for item in reversed(input_field):
            if isinstance(item, dict) and item.get("role") == "user":
                content = item.get("content") or ""
                if isinstance(content, str):
                    last_user_message = content
                elif isinstance(content, list):
                    last_user_message = "".join(
                        b.get("text", "")
                        for b in content
                        if isinstance(b, dict) and b.get("type") == "input_text"
                    )
                break
    return last_user_message

payloads = {
    "Responses structured input with input_text": [
        {"role": "user", "content": [{"type": "input_text", "text": "Last user input"}]},
        {"role": "assistant", "content": "assistant output"},
        {"role": "user", "content": [{"type": "input_text", "text": "Oldest user input"}]},
    ],
    "Responses with top-level text object": [
        {"role": "user", "content": [{"type": "text", "text": "User text"}]},
    ],
    "Responses plain strings": [
        "Last",
        "Oldest",
    ],
}
for name, input_field in payloads.items():
    current = extract_current(input_field)
    proposed = extract_proposed(input_field)
    print(f"{name}: current={current!r}, proposed={proposed!r}")
PY

Repository: sheepdestroyer/LLM-Routing

Length of output: 436


Extract the last user input before triage and support input_text content parts.

In responses_api, structured Responses payloads use type: "input_text" for user text, so the current type == "text" guards leave those inputs empty and force triage to instructions or an empty string. Also, chat_completions uses reversed(messages) and stops at the first user message, while this loop keeps concatenating all strings/content blocks in input, merging multiple turns. Fix both checks to use input_text and extract only the latest user turn before classifying.

🤖 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 `@router/main.py` around lines 1999 - 2020, Update the last-user-input
extraction in responses_api to recognize input_text for both top-level items and
nested content blocks, and change the input-list traversal to inspect items in
reverse and stop after extracting the latest user turn. Keep chat_completions
aligned with this behavior by selecting only the most recent user message rather
than concatenating multiple turns.

Comment thread router/main.py
Comment on lines +2050 to +2072
is_streaming = body_to_send.get("stream", False)
client = get_http_client()
url = f"{LITELLM_URL}/v1/responses"

logger.info(f"Proxying Responses API request for model={target_model} to {url}")

if is_streaming:
async def response_streamer():
try:
async with client.stream(
"POST",
url,
json=body_to_send,
headers=headers,
timeout=600.0,
) as resp:
async for chunk in resp.aiter_bytes():
yield chunk
except Exception as stream_err:
logger.error(f"Error during Responses API streaming proxy: {stream_err}")
raise

return StreamingResponse(response_streamer(), media_type="text/event-stream")

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Streaming path never checks the upstream status code — errors get silently returned as HTTP 200.

Unlike chat_completions' streaming branch (which uses client.build_request + client.send(stream=True) and explicitly checks r.status_code == 200 before returning a StreamingResponse, else raising HTTPException), responses_api's response_streamer() opens client.stream(...) and immediately starts yielding bytes with no status check. If LiteLLM returns a 400/401/429/500 for a streaming request, the client still receives HTTP 200 with media_type="text/event-stream", masking the error and breaking Home Assistant's ability to react to failures — this directly conflicts with the PR's stated goal of "correct propagation of ... errors."

🐛 Proposed fix (mirrors the existing chat_completions pattern)
     if is_streaming:
-        async def response_streamer():
-            try:
-                async with client.stream(
-                    "POST",
-                    url,
-                    json=body_to_send,
-                    headers=headers,
-                    timeout=600.0,
-                ) as resp:
-                    async for chunk in resp.aiter_bytes():
-                        yield chunk
-            except Exception as stream_err:
-                logger.error(f"Error during Responses API streaming proxy: {stream_err}")
-                raise
-
-        return StreamingResponse(response_streamer(), media_type="text/event-stream")
+        req = client.build_request(
+            "POST", url, json=body_to_send, headers=headers, timeout=600.0
+        )
+        resp = await client.send(req, stream=True)
+        if resp.status_code != 200:
+            error_body = await resp.aread()
+            await resp.aclose()
+            logger.warning(
+                f"Responses API stream failed ({resp.status_code}): {error_body[:300]}"
+            )
+            raise HTTPException(status_code=resp.status_code, detail="Responses proxy failed")
+
+        async def response_streamer():
+            try:
+                async for chunk in resp.aiter_bytes():
+                    yield chunk
+            except Exception as stream_err:
+                logger.error(f"Error during Responses API streaming proxy: {stream_err}")
+                raise
+            finally:
+                await resp.aclose()
+
+        return StreamingResponse(response_streamer(), media_type="text/event-stream")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
is_streaming = body_to_send.get("stream", False)
client = get_http_client()
url = f"{LITELLM_URL}/v1/responses"
logger.info(f"Proxying Responses API request for model={target_model} to {url}")
if is_streaming:
async def response_streamer():
try:
async with client.stream(
"POST",
url,
json=body_to_send,
headers=headers,
timeout=600.0,
) as resp:
async for chunk in resp.aiter_bytes():
yield chunk
except Exception as stream_err:
logger.error(f"Error during Responses API streaming proxy: {stream_err}")
raise
return StreamingResponse(response_streamer(), media_type="text/event-stream")
is_streaming = body_to_send.get("stream", False)
client = get_http_client()
url = f"{LITELLM_URL}/v1/responses"
logger.info(f"Proxying Responses API request for model={target_model} to {url}")
if is_streaming:
req = client.build_request(
"POST", url, json=body_to_send, headers=headers, timeout=600.0
)
resp = await client.send(req, stream=True)
if resp.status_code != 200:
error_body = await resp.aread()
await resp.aclose()
logger.warning(
f"Responses API stream failed ({resp.status_code}): {error_body[:300]}"
)
raise HTTPException(status_code=resp.status_code, detail="Responses proxy failed")
async def response_streamer():
try:
async for chunk in resp.aiter_bytes():
yield chunk
except Exception as stream_err:
logger.error(f"Error during Responses API streaming proxy: {stream_err}")
raise
finally:
await resp.aclose()
return StreamingResponse(response_streamer(), media_type="text/event-stream")
🤖 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 `@router/main.py` around lines 2050 - 2072, Update the streaming branch in
responses_api’s response_streamer to validate the upstream resp.status_code
before yielding any bytes. Propagate non-success responses as HTTP errors,
preserving the existing successful StreamingResponse behavior and matching the
status handling used by chat_completions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation litellm router

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add OpenAI Responses API tools support for Home Assistant

1 participant