From ceae6d7b6131cf3313e09484fb62669cafa1d458 Mon Sep 17 00:00:00 2001 From: axelray-dev <110029405+axelray-dev@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:39:15 +0800 Subject: [PATCH] fix: allow custom provider to configure thinking style (#4429) --- nanobot/config/schema.py | 1 + nanobot/providers/factory.py | 2 +- nanobot/providers/registry.py | 3 +- nanobot/webui/settings_api.py | 4 +- tests/providers/test_custom_thinking_style.py | 47 +++++++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/providers/test_custom_thinking_style.py diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 09fa1b02..89214fa0 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -182,6 +182,7 @@ class ProviderConfig(Base): extra_headers: dict[str, str] | None = None # Custom headers (e.g. APP-Code for AiHubMix) extra_body: dict[str, Any] | None = None # Extra provider request fields; shape depends on provider/API surface extra_query: dict[str, str] | None = None # Extra query params (e.g. api-version for Azure-style gateways) + thinking_style: str = "" # Thinking injection style for custom providers: "thinking_type", "enable_thinking", "reasoning_split" class BedrockProviderConfig(ProviderConfig): diff --git a/nanobot/providers/factory.py b/nanobot/providers/factory.py index 6201c07b..3758c81f 100644 --- a/nanobot/providers/factory.py +++ b/nanobot/providers/factory.py @@ -54,7 +54,7 @@ def _make_provider_core( if provider_name and not spec and p: if not p.api_base: raise ValueError(f"Provider '{provider_name}' requires api_base in config.") - spec = create_dynamic_spec(provider_name) + spec = create_dynamic_spec(provider_name, thinking_style=p.thinking_style if p else "") if spec and spec.is_transcription_only: raise ValueError(f"Provider '{provider_name}' only supports transcription.") backend = spec.backend if spec else "openai_compat" diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index cbfe9ba7..2e34d4f4 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -628,7 +628,7 @@ def find_by_name(name: str) -> ProviderSpec | None: return None -def create_dynamic_spec(name: str) -> ProviderSpec: +def create_dynamic_spec(name: str, *, thinking_style: str = "") -> ProviderSpec: """Create a dynamic ProviderSpec for custom user-defined providers.""" normalized = to_snake(name.replace("-", "_")) strip_prefixes = tuple(dict.fromkeys((name, normalized))) @@ -640,4 +640,5 @@ def create_dynamic_spec(name: str) -> ProviderSpec: backend="openai_compat", is_direct=True, strip_model_prefixes=strip_prefixes, + thinking_style=thinking_style, ) diff --git a/nanobot/webui/settings_api.py b/nanobot/webui/settings_api.py index e7ba94a4..139ed4cb 100644 --- a/nanobot/webui/settings_api.py +++ b/nanobot/webui/settings_api.py @@ -359,7 +359,7 @@ def _resolve_settings_provider( normalized = provider_name.replace("-", "_") for extra_name, provider_config in _dynamic_provider_items(config): if provider_name == extra_name or normalized == extra_name.replace("-", "_"): - return create_dynamic_spec(extra_name), extra_name, provider_config + return create_dynamic_spec(extra_name, thinking_style=provider_config.thinking_style), extra_name, provider_config return None @@ -739,7 +739,7 @@ def settings_payload( providers.append( _provider_settings_row( provider_key, - create_dynamic_spec(provider_key), + create_dynamic_spec(provider_key, thinking_style=provider_config.thinking_style), provider_config, ) ) diff --git a/tests/providers/test_custom_thinking_style.py b/tests/providers/test_custom_thinking_style.py new file mode 100644 index 00000000..f97d3ee7 --- /dev/null +++ b/tests/providers/test_custom_thinking_style.py @@ -0,0 +1,47 @@ +"""Tests for custom provider thinking_style config passthrough.""" + +from __future__ import annotations + +from nanobot.config.schema import ProviderConfig, ProvidersConfig +from nanobot.providers.registry import create_dynamic_spec + + +class TestCustomProviderThinkingStyle: + """Verify that thinking_style flows from config to ProviderSpec.""" + + def test_default_thinking_style_is_empty(self) -> None: + cfg = ProviderConfig() + assert cfg.thinking_style == "" + + def test_create_dynamic_spec_default(self) -> None: + spec = create_dynamic_spec("custom") + assert spec.thinking_style == "" + + def test_create_dynamic_spec_with_thinking_type(self) -> None: + spec = create_dynamic_spec("custom", thinking_style="thinking_type") + assert spec.thinking_style == "thinking_type" + + def test_create_dynamic_spec_with_enable_thinking(self) -> None: + spec = create_dynamic_spec("custom", thinking_style="enable_thinking") + assert spec.thinking_style == "enable_thinking" + + def test_create_dynamic_spec_with_reasoning_split(self) -> None: + spec = create_dynamic_spec("custom", thinking_style="reasoning_split") + assert spec.thinking_style == "reasoning_split" + + def test_provider_config_accepts_camel_case(self) -> None: + """Config JSON uses camelCase: thinkingStyle.""" + cfg = ProviderConfig.model_validate({"thinkingStyle": "thinking_type"}) + assert cfg.thinking_style == "thinking_type" + + def test_providers_config_custom_has_thinking_style(self) -> None: + """Full providers config round-trip.""" + data = { + "custom": { + "apiKey": "sk-test", + "apiBase": "https://example.com/v1", + "thinkingStyle": "enable_thinking", + } + } + pc = ProvidersConfig.model_validate(data) + assert pc.custom.thinking_style == "enable_thinking"