From a88850c180757bdf043ef990e16f995ebd34d98b Mon Sep 17 00:00:00 2001 From: matdev83 <211248003+matdev83@users.noreply.github.com> Date: Fri, 7 Nov 2025 22:17:50 +0100 Subject: [PATCH] Enhance OpenRouter payload handling --- src/connectors/openrouter.py | 63 +++++++++++++++++++ .../test_payload_construction_and_headers.py | 60 ++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/connectors/openrouter.py b/src/connectors/openrouter.py index 75a6f0112..db3f81cec 100644 --- a/src/connectors/openrouter.py +++ b/src/connectors/openrouter.py @@ -349,6 +349,25 @@ async def chat_completions( # type: ignore[override] ) # Add OpenRouter-specific parameters to the payload + def _normalize_payload_value(value: Any) -> Any: + if value is None: + return None + if hasattr(value, "model_dump") and callable(value.model_dump): + try: + return value.model_dump(exclude_none=True) + except TypeError: + return value.model_dump() + if isinstance(value, Mapping): + return { + key: _normalize_payload_value(val) + for key, val in value.items() + } + if isinstance(value, list): + return [_normalize_payload_value(item) for item in value] + if isinstance(value, tuple): + return [_normalize_payload_value(item) for item in value] + return value + if domain_request.top_k is not None: payload["top_k"] = domain_request.top_k if domain_request.seed is not None: @@ -361,6 +380,50 @@ async def chat_completions( # type: ignore[override] payload["frequency_penalty"] = domain_request.frequency_penalty if domain_request.presence_penalty is not None: payload["presence_penalty"] = domain_request.presence_penalty + if domain_request.repetition_penalty is not None: + payload["repetition_penalty"] = domain_request.repetition_penalty + if domain_request.top_logprobs is not None: + payload["top_logprobs"] = domain_request.top_logprobs + if domain_request.min_p is not None: + payload["min_p"] = domain_request.min_p + if domain_request.top_a is not None: + payload["top_a"] = domain_request.top_a + if domain_request.prediction is not None: + payload["prediction"] = _normalize_payload_value( + domain_request.prediction + ) + if domain_request.response_format is not None: + payload["response_format"] = _normalize_payload_value( + domain_request.response_format + ) + if domain_request.transforms: + transforms_value = domain_request.transforms + if isinstance(transforms_value, (list, tuple)): + payload["transforms"] = [ + _normalize_payload_value(item) + for item in transforms_value + ] + else: + payload["transforms"] = [ + _normalize_payload_value(transforms_value) + ] + if domain_request.models: + models_value = domain_request.models + if isinstance(models_value, (list, tuple)): + payload["models"] = [ + _normalize_payload_value(item) + for item in models_value + ] + else: + payload["models"] = [ + _normalize_payload_value(models_value) + ] + if domain_request.route is not None: + payload["route"] = domain_request.route + if domain_request.provider is not None: + payload["provider"] = _normalize_payload_value( + domain_request.provider + ) # Handle extra_body from the request (takes precedence) if hasattr(domain_request, "extra_body") and domain_request.extra_body: diff --git a/tests/unit/openrouter_connector_tests/test_payload_construction_and_headers.py b/tests/unit/openrouter_connector_tests/test_payload_construction_and_headers.py index 7828d3f14..aab7dcd4a 100644 --- a/tests/unit/openrouter_connector_tests/test_payload_construction_and_headers.py +++ b/tests/unit/openrouter_connector_tests/test_payload_construction_and_headers.py @@ -216,3 +216,63 @@ async def test_openrouter_processed_messages_remain_pydantic( assert isinstance( processed_msgs_fixture[2].content[1], MessageContentPartImage ) # Specific type + + +@pytest.mark.asyncio +async def test_openrouter_payload_extended_parameters( + openrouter_backend: OpenRouterBackend, + httpx_mock: HTTPXMock, + sample_chat_request_data: ChatRequest, +): + extended_request = sample_chat_request_data.model_copy( + update={ + "repetition_penalty": 1.1, + "top_logprobs": 3, + "min_p": 0.25, + "top_a": 0.9, + "prediction": {"type": "content", "content": "prefill"}, + "response_format": {"type": "json_object"}, + "transforms": ["normalize-prompts"], + "models": ["openai/gpt-4o", "openai/gpt-4o-mini"], + "route": "fallback", + "provider": { + "include": ["openai"], + "allow_fallbacks": True, + }, + "extra_body": {"transforms": ["override-transform"]}, + } + ) + + httpx_mock.add_response( + status_code=200, + json={"choices": [{"message": {"content": "ok"}}]}, + ) + + await openrouter_backend.chat_completions( + request_data=extended_request, + processed_messages=extended_request.messages, + effective_model=extended_request.model, + openrouter_api_base_url=TEST_OPENROUTER_API_BASE_URL, + openrouter_headers_provider=mock_get_openrouter_headers, + key_name="test_key", + api_key="FAKE_KEY", + ) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + + payload = json.loads(sent_request.content) + assert payload["repetition_penalty"] == 1.1 + assert payload["top_logprobs"] == 3 + assert payload["min_p"] == 0.25 + assert payload["top_a"] == 0.9 + assert payload["prediction"] == {"type": "content", "content": "prefill"} + assert payload["response_format"] == {"type": "json_object"} + assert payload["models"] == ["openai/gpt-4o", "openai/gpt-4o-mini"] + assert payload["route"] == "fallback" + assert payload["provider"] == { + "include": ["openai"], + "allow_fallbacks": True, + } + # extra_body should take precedence for transforms + assert payload["transforms"] == ["override-transform"]