Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/connectors/openrouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Comment on lines +383 to +384

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Access OpenRouter fields missing from ChatRequest

The new payload section assumes the request model already defines attributes such as repetition_penalty, top_logprobs, min_p, transforms, etc. However ChatRequest/CanonicalChatRequest in src/core/domain/chat.py do not declare these fields and Pydantic ignores unknown keys when copying. As a result a normal call hits domain_request.repetition_penalty here and raises AttributeError, so every OpenRouter chat request crashes before sending the HTTP call. The parameters need to be added to the domain model or accessed via getattr(..., None) to tolerate missing fields.

Useful? React with 👍 / 👎.

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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Loading