fix(providers): send DeepInfra reasoning effort
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
"""DeepInfra provider profile (chat surface; image-gen/TTS/STT are wired via
|
||||
their own plugin subsystems)."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent.reasoning_effort import (
|
||||
OPENAI_COMPAT_WIRE_EFFORTS,
|
||||
clamp_effort,
|
||||
requested_effort,
|
||||
)
|
||||
from providers import register_provider
|
||||
from providers.base import ProviderProfile
|
||||
|
||||
@@ -9,6 +16,29 @@ class _DeepInfraProfile(ProviderProfile):
|
||||
"""DeepInfra profile with live vision-default discovery, so shared vision
|
||||
resolution in ``agent/auxiliary_client.py`` stays provider-agnostic."""
|
||||
|
||||
def build_api_kwargs_extras(
|
||||
self, *, reasoning_config: dict | None = None, **context: Any
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""Map Hermes reasoning controls to DeepInfra's top-level wire field.
|
||||
|
||||
DeepInfra applies a per-model default when the field is absent, while
|
||||
``none`` is its explicit off switch. This route does not advertise a
|
||||
reasoning capability to the shared transport, so it must not be gated
|
||||
on ``supports_reasoning``.
|
||||
"""
|
||||
if (
|
||||
isinstance(reasoning_config, dict)
|
||||
and reasoning_config.get("enabled") is False
|
||||
):
|
||||
return {}, {"reasoning_effort": "none"}
|
||||
effort = requested_effort(reasoning_config)
|
||||
clamped = clamp_effort(effort, OPENAI_COMPAT_WIRE_EFFORTS)
|
||||
return (
|
||||
({}, {"reasoning_effort": clamped})
|
||||
if clamped in OPENAI_COMPAT_WIRE_EFFORTS
|
||||
else ({}, {})
|
||||
)
|
||||
|
||||
def default_vision_model(self): # type: ignore[override]
|
||||
"""First vision-capable *chat* model from the live catalog, or None. Key-gated so a box
|
||||
without DEEPINFRA_API_KEY never pays the round-trip; requires the ``chat`` surface tag so
|
||||
@@ -19,6 +49,7 @@ class _DeepInfraProfile(ProviderProfile):
|
||||
return None
|
||||
try:
|
||||
from hermes_cli.models import _fetch_deepinfra_models_by_tag
|
||||
|
||||
items = _fetch_deepinfra_models_by_tag("chat")
|
||||
except Exception:
|
||||
return None
|
||||
@@ -31,9 +62,13 @@ class _DeepInfraProfile(ProviderProfile):
|
||||
|
||||
|
||||
deepinfra = _DeepInfraProfile(
|
||||
name="deepinfra", aliases=("deep-infra", "deepinfra-ai"), display_name="DeepInfra",
|
||||
description="DeepInfra — 100+ open models, pay-per-use", signup_url="https://deepinfra.com/dash/api_keys",
|
||||
env_vars=("DEEPINFRA_API_KEY", "DEEPINFRA_BASE_URL"), base_url="https://api.deepinfra.com/v1/openai",
|
||||
name="deepinfra",
|
||||
aliases=("deep-infra", "deepinfra-ai"),
|
||||
display_name="DeepInfra",
|
||||
description="DeepInfra — 100+ open models, pay-per-use",
|
||||
signup_url="https://deepinfra.com/dash/api_keys",
|
||||
env_vars=("DEEPINFRA_API_KEY", "DEEPINFRA_BASE_URL"),
|
||||
base_url="https://api.deepinfra.com/v1/openai",
|
||||
auth_type="api_key",
|
||||
default_max_tokens=None, # DeepInfra applies its documented per-model limit
|
||||
# The only hardcoded DeepInfra model: aux resolution is synchronous, so it
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Regression coverage for DeepInfra's top-level reasoning_effort wire field."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deepinfra_profile():
|
||||
"""Resolve the registered profile through the normal plugin discovery path."""
|
||||
import model_tools # noqa: F401
|
||||
import providers
|
||||
|
||||
profile = providers.get_provider_profile("deepinfra")
|
||||
assert profile is not None, "deepinfra provider profile must be registered"
|
||||
return profile
|
||||
|
||||
|
||||
class TestDeepInfraReasoningEffort:
|
||||
def test_default_preserves_the_model_default(self, deepinfra_profile):
|
||||
assert deepinfra_profile.build_api_kwargs_extras(reasoning_config=None) == (
|
||||
{},
|
||||
{},
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"effort", ("minimal", "low", "medium", "high", "xhigh", "max")
|
||||
)
|
||||
def test_explicit_efforts_are_sent_verbatim(self, deepinfra_profile, effort):
|
||||
assert deepinfra_profile.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": True, "effort": effort}
|
||||
) == ({}, {"reasoning_effort": effort})
|
||||
|
||||
def test_ultra_clamps_to_deepinfra_maximum(self, deepinfra_profile):
|
||||
assert deepinfra_profile.build_api_kwargs_extras(
|
||||
reasoning_config={"enabled": True, "effort": "ultra"}
|
||||
) == ({}, {"reasoning_effort": "max"})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reasoning_config", ({"enabled": False}, {"enabled": False, "effort": "high"})
|
||||
)
|
||||
def test_disabled_sends_the_explicit_off_value(
|
||||
self, deepinfra_profile, reasoning_config
|
||||
):
|
||||
assert deepinfra_profile.build_api_kwargs_extras(
|
||||
reasoning_config=reasoning_config
|
||||
) == ({}, {"reasoning_effort": "none"})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reasoning_config",
|
||||
({}, {"enabled": True}, {"enabled": True, "effort": "future-tier"}),
|
||||
)
|
||||
def test_missing_or_unknown_effort_preserves_the_model_default(
|
||||
self, deepinfra_profile, reasoning_config
|
||||
):
|
||||
assert deepinfra_profile.build_api_kwargs_extras(
|
||||
reasoning_config=reasoning_config
|
||||
) == ({}, {})
|
||||
|
||||
def test_transport_includes_top_level_reasoning_effort_without_capability_gate(
|
||||
self, deepinfra_profile
|
||||
):
|
||||
from agent.transports.chat_completions import ChatCompletionsTransport
|
||||
|
||||
kwargs = ChatCompletionsTransport().build_kwargs(
|
||||
model="deepseek-ai/DeepSeek-V4.1-Flash",
|
||||
messages=[{"role": "user", "content": "ping"}],
|
||||
tools=None,
|
||||
provider_profile=deepinfra_profile,
|
||||
provider_name="deepinfra",
|
||||
reasoning_config={"enabled": True, "effort": "high"},
|
||||
supports_reasoning=False,
|
||||
)
|
||||
assert kwargs["reasoning_effort"] == "high"
|
||||
Reference in New Issue
Block a user