diff --git a/skillopt_sleep/backend.py b/skillopt_sleep/backend.py index 768a4e86..fda126da 100644 --- a/skillopt_sleep/backend.py +++ b/skillopt_sleep/backend.py @@ -1311,8 +1311,12 @@ def _read_chat_extra_body() -> Optional[Dict[str, Any]]: def _is_azure_host(self) -> bool: from urllib.parse import urlparse - host = (urlparse(self.endpoint).hostname or "").lower() - return host.endswith(self._AZURE_HOST_SUFFIXES) + parsed = urlparse(self.endpoint) + host = (parsed.hostname or "").lower() + return ( + parsed.scheme.lower() == "https" + and host.endswith(self._AZURE_HOST_SUFFIXES) + ) def _get_client(self): if self._client is None: @@ -1383,7 +1387,7 @@ def _call(self, prompt: str, *, max_tokens: int = 1024, retries: int = 5) -> str kwargs["max_tokens"] = self.compat_max_tokens else: kwargs["max_completion_tokens"] = 16384 - if self.chat_extra_body: + if self._compat_mode() and self.chat_extra_body: kwargs["extra_body"] = self.chat_extra_body resp = client.chat.completions.create(**kwargs) text = (resp.choices[0].message.content or "").strip() diff --git a/tests/test_azure_openai_compat.py b/tests/test_azure_openai_compat.py index bc4e6101..cd9d2882 100644 --- a/tests/test_azure_openai_compat.py +++ b/tests/test_azure_openai_compat.py @@ -108,6 +108,17 @@ def test_managed_identity_refuses_non_azure_endpoint(self): be._get_client() self.assertIn("openai_compatible", str(ctx.exception)) + def test_managed_identity_refuses_insecure_azure_endpoint(self): + # A matching Azure hostname is insufficient: AAD bearer credentials + # must never be sent over plaintext HTTP. + env = {"AZURE_OPENAI_ENDPOINT": "http://foo.openai.azure.com"} + with mock.patch.dict(os.environ, env, clear=True): + be = AzureOpenAIBackend(deployment="some-model") + self.assertFalse(be._is_azure_host()) + with self.assertRaises(ValueError) as ctx: + be._get_client() + self.assertIn("openai_compatible", str(ctx.exception)) + def test_azure_host_detection(self): with mock.patch.dict(os.environ, {}, clear=True): be = AzureOpenAIBackend(deployment="gpt-5.5") # table endpoint @@ -189,6 +200,15 @@ def test_azure_mode_sends_max_completion_tokens(self): self.assertEqual(call["max_completion_tokens"], 16384) self.assertNotIn("max_tokens", call) + def test_azure_mode_ignores_compat_extra_body(self): + body = {"thinking": {"type": "enabled"}} + env = {"SKILLOPT_SLEEP_CHAT_EXTRA_BODY": json.dumps(body)} + be = _backend_with(["hi"], env) + with mock.patch.dict(os.environ, env, clear=True): + be._call("p", retries=1) + (call,) = be._client.chat.completions.calls + self.assertNotIn("extra_body", call) + class TestErrorState(unittest.TestCase): def test_recovered_retry_clears_last_call_error(self):