feat: Enhance OpenAI provider configuration with extraBody support and apiType validation
This commit is contained in:
@@ -30,7 +30,7 @@ from nanobot.channels.websocket import (
|
||||
)
|
||||
from nanobot.config.loader import load_config, save_config
|
||||
from nanobot.config.schema import Config, ModelPresetConfig
|
||||
from nanobot.webui.settings_api import settings_payload
|
||||
from nanobot.webui.settings_api import settings_payload, update_provider_settings
|
||||
|
||||
# -- Shared helpers (aligned with test_websocket_integration.py) ---------------
|
||||
|
||||
@@ -1351,6 +1351,37 @@ def test_settings_payload_normalizes_camel_case_provider(
|
||||
assert body["agent"]["provider"] == "minimax_anthropic"
|
||||
|
||||
|
||||
def test_settings_payload_exposes_api_type_only_for_openai(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.providers.openai.api_type = "responses"
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
body = settings_payload()
|
||||
providers = {provider["name"]: provider for provider in body["providers"]}
|
||||
|
||||
assert providers["openai"]["api_type"] == "responses"
|
||||
assert "api_type" not in providers["custom"]
|
||||
|
||||
|
||||
def test_update_provider_settings_ignores_api_type_for_non_openai(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
body = update_provider_settings({
|
||||
"provider": ["custom"],
|
||||
"api_base": ["https://example.test/v1"],
|
||||
"api_type": ["responses"],
|
||||
})
|
||||
|
||||
assert body["providers"]
|
||||
config = load_config(config_path)
|
||||
assert config.providers.custom.api_base == "https://example.test/v1"
|
||||
assert config.providers.custom.api_type == "auto"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_end_to_end_server_pushes_streaming_deltas_to_client(bus: MagicMock) -> None:
|
||||
port = 29880
|
||||
|
||||
@@ -36,6 +36,18 @@ def test_provider_api_type_accepts_exact_values_only() -> None:
|
||||
})
|
||||
|
||||
|
||||
def test_provider_api_type_is_openai_only() -> None:
|
||||
with pytest.raises(ValueError, match="only supported"):
|
||||
Config.model_validate({
|
||||
"providers": {
|
||||
"custom": {
|
||||
"apiBase": "https://example.test/v1",
|
||||
"apiType": "responses",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def test_legacy_defaults_config_without_presets_still_resolves() -> None:
|
||||
config = Config.model_validate({
|
||||
"agents": {
|
||||
|
||||
@@ -9,6 +9,7 @@ from nanobot.providers.openai_compat_provider import (
|
||||
OpenAICompatProvider,
|
||||
_deep_merge,
|
||||
)
|
||||
from nanobot.providers.registry import find_by_name
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _deep_merge unit tests
|
||||
@@ -185,6 +186,86 @@ class TestBuildKwargsExtraBody:
|
||||
assert kwargs["extra_body"]["repetition_penalty"] == 1.15
|
||||
|
||||
|
||||
class TestBuildResponsesBodyExtraBody:
|
||||
"""Verify extra_body flows into Responses API request bodies."""
|
||||
|
||||
def test_responses_extra_body_merges_top_level_fields(self) -> None:
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="test-key",
|
||||
default_model="gpt-5",
|
||||
spec=find_by_name("openai"),
|
||||
extra_body={
|
||||
"metadata": {"source": "test"},
|
||||
"parallel_tool_calls": False,
|
||||
},
|
||||
)
|
||||
|
||||
body = provider._build_responses_body(
|
||||
messages=_simple_messages(),
|
||||
tools=None, model=None, max_tokens=100,
|
||||
temperature=0.1, reasoning_effort=None, tool_choice=None,
|
||||
)
|
||||
|
||||
assert body["metadata"] == {"source": "test"}
|
||||
assert body["parallel_tool_calls"] is False
|
||||
|
||||
def test_responses_extra_body_appends_tools(self) -> None:
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="test-key",
|
||||
default_model="gpt-5",
|
||||
spec=find_by_name("openai"),
|
||||
extra_body={"tools": [{"type": "web_search"}]},
|
||||
)
|
||||
|
||||
body = provider._build_responses_body(
|
||||
messages=_simple_messages(),
|
||||
tools=[{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {"type": "object"},
|
||||
},
|
||||
}],
|
||||
model=None, max_tokens=100, temperature=0.1,
|
||||
reasoning_effort=None, tool_choice=None,
|
||||
)
|
||||
|
||||
assert body["tools"] == [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {"type": "object"},
|
||||
},
|
||||
{"type": "web_search"},
|
||||
]
|
||||
|
||||
def test_responses_extra_body_merges_include_without_duplicates(self) -> None:
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="test-key",
|
||||
default_model="gpt-5",
|
||||
spec=find_by_name("openai"),
|
||||
extra_body={
|
||||
"include": [
|
||||
"reasoning.encrypted_content",
|
||||
"web_search_call.action.sources",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
body = provider._build_responses_body(
|
||||
messages=_simple_messages(),
|
||||
tools=None, model=None, max_tokens=100,
|
||||
temperature=0.1, reasoning_effort="high", tool_choice=None,
|
||||
)
|
||||
|
||||
assert body["include"] == [
|
||||
"reasoning.encrypted_content",
|
||||
"web_search_call.action.sources",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -39,6 +39,13 @@ def test_api_type_responses_forces_responses_for_openai(provider):
|
||||
assert provider._should_use_responses_api("gpt-4o", None) is True
|
||||
|
||||
|
||||
def test_api_type_responses_does_not_force_non_openai(provider):
|
||||
provider._spec = type("Spec", (), {"name": "custom"})()
|
||||
provider._api_type = "responses"
|
||||
|
||||
assert provider._should_use_responses_api("gpt-4o", None) is False
|
||||
|
||||
|
||||
def test_circuit_opens_after_threshold(provider):
|
||||
for _ in range(_RESPONSES_FAILURE_THRESHOLD):
|
||||
provider._record_responses_failure("gpt-5", None)
|
||||
|
||||
Reference in New Issue
Block a user