fix(openrouter): pass custom fields through SDK extra_body (#5703)

Passing OpenRouter-specific fields such as `extra_body={"provider":
{"order": ["test-provider"]}}` currently fails before any HTTP request
with `AsyncCompletions.create() got an unexpected keyword argument
'provider'`.

Both text and structured-output paths unpack `extra_body` into SDK
keyword arguments. Pass it through the SDK's `extra_body` parameter
instead, so custom fields become part of the JSON request body.

Regression tests exercise the real OpenAI SDK with an HTTPX mock
transport and verify custom fields reach the outgoing JSON for both
output modes. They also cover omitted and empty extra bodies and
structured response parsing.

Validation:
- Before the fix: 2 regression failures, 8 passing tests in the
OpenRouter model suite.
- After the fix: 15 tests passed across the OpenRouter model and
token-cost suites.
- All applicable pre-commit hooks passed, including Ruff and Pyright.
- No real API key or live provider request was used.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes passing OpenRouter-specific fields through the SDK's `extra_body`
parameter so custom fields like provider routing reach the HTTP request
body instead of failing before any request is sent.

- Text and structured-output paths now forward `extra_body` instead of
unpacking it into SDK keyword arguments.
- Adds regression tests with a mock HTTP transport covering both output
modes and omitted or empty `extra_body` values.

<sup>Written for commit d05053ed60.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5703?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Magnus Müller
2026-09-07 11:12:49 -07:00
committed by GitHub
2 changed files with 34 additions and 2 deletions
+2 -2
View File
@@ -167,7 +167,7 @@ class ChatOpenRouter(BaseChatModel):
top_p=self.top_p,
seed=self.seed,
extra_headers=extra_headers,
**(self.extra_body or {}),
extra_body=self.extra_body,
)
choice = self._get_first_choice(response)
@@ -199,7 +199,7 @@ class ChatOpenRouter(BaseChatModel):
type='json_schema',
),
extra_headers=extra_headers,
**(self.extra_body or {}),
extra_body=self.extra_body,
)
choice = self._get_first_choice(response)
+32
View File
@@ -1,7 +1,9 @@
"""Regression tests for OpenRouter client setup and response handling."""
import json
from unittest.mock import AsyncMock, patch
import httpx
import pytest
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
@@ -16,6 +18,36 @@ class Answer(BaseModel):
answer: str
@pytest.mark.parametrize('structured', [False, True])
@pytest.mark.parametrize('extra_body', [None, {}, {'provider': {'order': ['test-provider']}, 'transforms': ['middle-out']}])
async def test_extra_body_reaches_http_request(structured: bool, extra_body: dict | None):
"""Send provider-specific fields through the real SDK into the JSON request body."""
requests: list[httpx.Request] = []
def handle_request(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, json=_completion(content='{"answer":"ok"}' if structured else 'ok').model_dump())
async with httpx.AsyncClient(transport=httpx.MockTransport(handle_request)) as client:
llm = ChatOpenRouter(model='openai/gpt-4o', api_key='test-key', http_client=client, extra_body=extra_body)
result = await llm.ainvoke([UserMessage(content='question')], output_format=Answer if structured else None)
assert result.completion == (Answer(answer='ok') if structured else 'ok')
assert len(requests) == 1
body = json.loads(requests[0].content)
assert body['model'] == 'openai/gpt-4o'
assert body['messages'] == [{'role': 'user', 'content': 'question'}]
assert 'extra_body' not in body
if extra_body:
assert body['provider'] == {'order': ['test-provider']}
assert body['transforms'] == ['middle-out']
else:
assert 'provider' not in body
assert 'transforms' not in body
if structured:
assert body['response_format']['type'] == 'json_schema'
def _completion(*, content: str | None = 'ok', choices: bool = True) -> ChatCompletion:
return ChatCompletion(
id='chatcmpl-test',