diff --git a/skillopt_sleep/backend.py b/skillopt_sleep/backend.py index 768a4e8..fda126d 100644 --- a/skillopt_sleep/backend.py +++ b/skillopt_sleep/backend.py @@ -1311,8 +1311,12 @@ class AzureOpenAIBackend(CliBackend): 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 @@ class AzureOpenAIBackend(CliBackend): 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 bc4e610..cd9d288 100644 --- a/tests/test_azure_openai_compat.py +++ b/tests/test_azure_openai_compat.py @@ -108,6 +108,17 @@ class TestClientSelection(unittest.TestCase): 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 @@ class TestRequestKwargs(unittest.TestCase): 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):