From ebbd91de2e9ac2110a0446540ca17e6de39f2752 Mon Sep 17 00:00:00 2001 From: Kevin-Li-2025 <2242139@qq.com> Date: Sat, 27 Jun 2026 19:02:08 +0800 Subject: [PATCH 1/2] Fix OpenAI structured chat response handling --- common/llm/adapters/azure_openai.py | 3 +- tests/test_azure_openai_adapter.py | 90 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/test_azure_openai_adapter.py diff --git a/common/llm/adapters/azure_openai.py b/common/llm/adapters/azure_openai.py index 9d0290a0..687110b3 100644 --- a/common/llm/adapters/azure_openai.py +++ b/common/llm/adapters/azure_openai.py @@ -37,7 +37,8 @@ async def structured_chat(self, messages: list[dict[str, str]], response_format: response = await self.async_azure_client.beta.chat.completions.parse( model=self._model, messages=messages, response_format=response_format, **self._kwargs ) - choice = self.handle_response(response) + choice = response.choices[0] + self.choice_incomplete(choice, response) return choice.message.parsed diff --git a/tests/test_azure_openai_adapter.py b/tests/test_azure_openai_adapter.py new file mode 100644 index 00000000..5900ab40 --- /dev/null +++ b/tests/test_azure_openai_adapter.py @@ -0,0 +1,90 @@ +import importlib +from types import SimpleNamespace +from typing import Any + +import pytest +from pydantic import BaseModel + + +class House(BaseModel): + color: str + + +class FakeParsedCompletions: + def __init__(self, response: Any) -> None: + self.response = response + self.calls: list[dict[str, Any]] = [] + + async def parse(self, **kwargs: Any) -> Any: + self.calls.append(kwargs) + return self.response + + +@pytest.fixture +def azure_openai_module(monkeypatch): + required_settings = { + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_DB": "minute", + "POSTGRES_USER": "minute", + "POSTGRES_PASSWORD": "minute", + "APP_URL": "http://localhost", + "REPO": "minute", + "AUTH_API_URL": "http://localhost/auth", + "TRANSCRIPTION_QUEUE_NAME": "transcription", + "TRANSCRIPTION_DEADLETTER_QUEUE_NAME": "transcription-dlq", + "LLM_QUEUE_NAME": "llm", + "LLM_DEADLETTER_QUEUE_NAME": "llm-dlq", + "AZURE_SPEECH_KEY": "dummy", + "AZURE_SPEECH_REGION": "uksouth", + } + for key, value in required_settings.items(): + monkeypatch.setenv(key, value) + return importlib.import_module("common.llm.adapters.azure_openai") + + +@pytest.mark.asyncio +async def test_structured_chat_returns_parsed_choice(azure_openai_module, monkeypatch) -> None: + parsed = House(color="red") + response = SimpleNamespace( + choices=[SimpleNamespace(finish_reason="stop", message=SimpleNamespace(parsed=parsed))] + ) + completions = FakeParsedCompletions(response) + azure_client_calls = [] + + def fake_azure_client(**kwargs: Any) -> Any: + azure_client_calls.append(kwargs) + return SimpleNamespace(beta=SimpleNamespace(chat=SimpleNamespace(completions=completions))) + + monkeypatch.setattr(azure_openai_module, "AsyncAzureOpenAI", fake_azure_client) + + adapter = azure_openai_module.OpenAIModelAdapter( + model="deployment-name", + api_key="api-key", + azure_endpoint="https://example.com", + azure_deployment="deployment-name", + temperature=0.0, + ) + + result = await adapter.structured_chat( + messages=[{"role": "user", "content": "Describe the house"}], + response_format=House, + ) + + assert azure_client_calls == [ + { + "azure_endpoint": "https://example.com", + "api_key": "api-key", + "api_version": "2024-08-01-preview", + "azure_deployment": "deployment-name", + } + ] + assert result == parsed + assert completions.calls == [ + { + "model": "deployment-name", + "messages": [{"role": "user", "content": "Describe the house"}], + "response_format": House, + "temperature": 0.0, + } + ] From 025192a61f52501f64b17c4b9220bc144630a495 Mon Sep 17 00:00:00 2001 From: Kevin-Li-2025 Date: Fri, 24 Jul 2026 01:33:55 +0800 Subject: [PATCH 2/2] test: verify structured response completeness check Signed-off-by: Kevin-Li-2025 --- tests/test_azure_openai_adapter.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_azure_openai_adapter.py b/tests/test_azure_openai_adapter.py index 5900ab40..e79cc6fb 100644 --- a/tests/test_azure_openai_adapter.py +++ b/tests/test_azure_openai_adapter.py @@ -46,17 +46,21 @@ def azure_openai_module(monkeypatch): @pytest.mark.asyncio async def test_structured_chat_returns_parsed_choice(azure_openai_module, monkeypatch) -> None: parsed = House(color="red") - response = SimpleNamespace( - choices=[SimpleNamespace(finish_reason="stop", message=SimpleNamespace(parsed=parsed))] - ) + response = SimpleNamespace(choices=[SimpleNamespace(finish_reason="stop", message=SimpleNamespace(parsed=parsed))]) completions = FakeParsedCompletions(response) azure_client_calls = [] + incomplete_checks = [] def fake_azure_client(**kwargs: Any) -> Any: azure_client_calls.append(kwargs) return SimpleNamespace(beta=SimpleNamespace(chat=SimpleNamespace(completions=completions))) monkeypatch.setattr(azure_openai_module, "AsyncAzureOpenAI", fake_azure_client) + monkeypatch.setattr( + azure_openai_module.OpenAIModelAdapter, + "choice_incomplete", + staticmethod(lambda choice, completion: incomplete_checks.append((choice, completion))), + ) adapter = azure_openai_module.OpenAIModelAdapter( model="deployment-name", @@ -80,6 +84,7 @@ def fake_azure_client(**kwargs: Any) -> Any: } ] assert result == parsed + assert incomplete_checks == [(response.choices[0], response)] assert completions.calls == [ { "model": "deployment-name",