Skip to content

Commit a88850c

Browse files
committed
Enhance OpenRouter payload handling
1 parent c03159e commit a88850c

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

src/connectors/openrouter.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,25 @@ async def chat_completions( # type: ignore[override]
349349
)
350350

351351
# Add OpenRouter-specific parameters to the payload
352+
def _normalize_payload_value(value: Any) -> Any:
353+
if value is None:
354+
return None
355+
if hasattr(value, "model_dump") and callable(value.model_dump):
356+
try:
357+
return value.model_dump(exclude_none=True)
358+
except TypeError:
359+
return value.model_dump()
360+
if isinstance(value, Mapping):
361+
return {
362+
key: _normalize_payload_value(val)
363+
for key, val in value.items()
364+
}
365+
if isinstance(value, list):
366+
return [_normalize_payload_value(item) for item in value]
367+
if isinstance(value, tuple):
368+
return [_normalize_payload_value(item) for item in value]
369+
return value
370+
352371
if domain_request.top_k is not None:
353372
payload["top_k"] = domain_request.top_k
354373
if domain_request.seed is not None:
@@ -361,6 +380,50 @@ async def chat_completions( # type: ignore[override]
361380
payload["frequency_penalty"] = domain_request.frequency_penalty
362381
if domain_request.presence_penalty is not None:
363382
payload["presence_penalty"] = domain_request.presence_penalty
383+
if domain_request.repetition_penalty is not None:
384+
payload["repetition_penalty"] = domain_request.repetition_penalty
385+
if domain_request.top_logprobs is not None:
386+
payload["top_logprobs"] = domain_request.top_logprobs
387+
if domain_request.min_p is not None:
388+
payload["min_p"] = domain_request.min_p
389+
if domain_request.top_a is not None:
390+
payload["top_a"] = domain_request.top_a
391+
if domain_request.prediction is not None:
392+
payload["prediction"] = _normalize_payload_value(
393+
domain_request.prediction
394+
)
395+
if domain_request.response_format is not None:
396+
payload["response_format"] = _normalize_payload_value(
397+
domain_request.response_format
398+
)
399+
if domain_request.transforms:
400+
transforms_value = domain_request.transforms
401+
if isinstance(transforms_value, (list, tuple)):
402+
payload["transforms"] = [
403+
_normalize_payload_value(item)
404+
for item in transforms_value
405+
]
406+
else:
407+
payload["transforms"] = [
408+
_normalize_payload_value(transforms_value)
409+
]
410+
if domain_request.models:
411+
models_value = domain_request.models
412+
if isinstance(models_value, (list, tuple)):
413+
payload["models"] = [
414+
_normalize_payload_value(item)
415+
for item in models_value
416+
]
417+
else:
418+
payload["models"] = [
419+
_normalize_payload_value(models_value)
420+
]
421+
if domain_request.route is not None:
422+
payload["route"] = domain_request.route
423+
if domain_request.provider is not None:
424+
payload["provider"] = _normalize_payload_value(
425+
domain_request.provider
426+
)
364427

365428
# Handle extra_body from the request (takes precedence)
366429
if hasattr(domain_request, "extra_body") and domain_request.extra_body:

tests/unit/openrouter_connector_tests/test_payload_construction_and_headers.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,63 @@ async def test_openrouter_processed_messages_remain_pydantic(
216216
assert isinstance(
217217
processed_msgs_fixture[2].content[1], MessageContentPartImage
218218
) # Specific type
219+
220+
221+
@pytest.mark.asyncio
222+
async def test_openrouter_payload_extended_parameters(
223+
openrouter_backend: OpenRouterBackend,
224+
httpx_mock: HTTPXMock,
225+
sample_chat_request_data: ChatRequest,
226+
):
227+
extended_request = sample_chat_request_data.model_copy(
228+
update={
229+
"repetition_penalty": 1.1,
230+
"top_logprobs": 3,
231+
"min_p": 0.25,
232+
"top_a": 0.9,
233+
"prediction": {"type": "content", "content": "prefill"},
234+
"response_format": {"type": "json_object"},
235+
"transforms": ["normalize-prompts"],
236+
"models": ["openai/gpt-4o", "openai/gpt-4o-mini"],
237+
"route": "fallback",
238+
"provider": {
239+
"include": ["openai"],
240+
"allow_fallbacks": True,
241+
},
242+
"extra_body": {"transforms": ["override-transform"]},
243+
}
244+
)
245+
246+
httpx_mock.add_response(
247+
status_code=200,
248+
json={"choices": [{"message": {"content": "ok"}}]},
249+
)
250+
251+
await openrouter_backend.chat_completions(
252+
request_data=extended_request,
253+
processed_messages=extended_request.messages,
254+
effective_model=extended_request.model,
255+
openrouter_api_base_url=TEST_OPENROUTER_API_BASE_URL,
256+
openrouter_headers_provider=mock_get_openrouter_headers,
257+
key_name="test_key",
258+
api_key="FAKE_KEY",
259+
)
260+
261+
sent_request = httpx_mock.get_request()
262+
assert sent_request is not None
263+
264+
payload = json.loads(sent_request.content)
265+
assert payload["repetition_penalty"] == 1.1
266+
assert payload["top_logprobs"] == 3
267+
assert payload["min_p"] == 0.25
268+
assert payload["top_a"] == 0.9
269+
assert payload["prediction"] == {"type": "content", "content": "prefill"}
270+
assert payload["response_format"] == {"type": "json_object"}
271+
assert payload["models"] == ["openai/gpt-4o", "openai/gpt-4o-mini"]
272+
assert payload["route"] == "fallback"
273+
assert payload["provider"] == {
274+
"include": ["openai"],
275+
"allow_fallbacks": True,
276+
}
277+
# extra_body should take precedence for transforms
278+
assert payload["transforms"] == ["override-transform"]

0 commit comments

Comments
 (0)