from __future__ import annotations from datetime import UTC, datetime from types import SimpleNamespace import unittest from unittest.mock import AsyncMock, patch import httpx from openai import AsyncOpenAI, BadRequestError, ConflictError, NotFoundError from app import app from routes import openai_models, openai_responses from services.openai_realtime.auth import RealtimeAuthError, RealtimeCredential from services.openai_responses.protocol import ResponsesAPIError class OpenAIPythonSDKContractTests(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.rows: dict[str, SimpleNamespace] = {} self.run_count = 0 self._patches = [ patch.object( openai_responses, "_credential", new=AsyncMock(side_effect=self._credential), ), patch.object( openai_responses, "prepare_response", new=AsyncMock(side_effect=self._prepare_response), ), patch.object( openai_responses, "_run_prepared", new=AsyncMock(side_effect=self._run_prepared), ), patch.object( openai_responses, "get_response", new=AsyncMock(side_effect=self._get_response), ), patch.object( openai_responses, "get_idempotent_response", new=AsyncMock(side_effect=self._get_idempotent_response), ), patch.object( openai_responses, "delete_response", new=AsyncMock(side_effect=self._delete_response), ), patch.object( openai_models, "_authenticate", new=AsyncMock(side_effect=self._model_authenticate), ), patch.object( openai_models, "SessionLocal", side_effect=lambda: self._model_session(), ), ] for patcher in self._patches: patcher.start() self.http_client = httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://contract.test", ) self.client = AsyncOpenAI( api_key="sk-rt-key-a", base_url="http://contract.test/v1", http_client=self.http_client, max_retries=0, ) def _model_session(self): assistant = SimpleNamespace( id="asst_contract", name="Contract assistant", type="prompt", runtime_mode="pipeline", created_at=datetime.now(UTC), ) class Scalars: @staticmethod def all(): return [assistant] class Result: @staticmethod def scalars(): return Scalars() class Session: async def __aenter__(self): return self async def __aexit__(self, *_args): return None async def execute(self, _statement): return Result() return Session() async def asyncTearDown(self): await self.client.close() for patcher in reversed(self._patches): patcher.stop() async def _credential(self, request): token = request.headers["authorization"].removeprefix("Bearer ") return RealtimeCredential(api_key_id=token.removeprefix("sk-rt-")) async def _model_authenticate(self, request): if "authorization" not in request.headers: raise RealtimeAuthError("Missing bearer token") async def _prepare_response( self, request, *, api_key_id, idempotency_key=None, request_hash=None, ): parent = self.rows.get(request.previous_response_id) assistant_id = ( parent.assistant_id if parent is not None else str(request.model).removeprefix("assistant:") ) index = len(self.rows) + 1 row = SimpleNamespace( id=f"resp_contract_{index}", api_key_id=api_key_id, assistant_id=assistant_id, conversation_id=( parent.conversation_id if parent else f"conv_contract_{index}" ), previous_response_id=request.previous_response_id, status="in_progress", error=None, usage=None, input_items=request.input_items, output_items=[], created_at=datetime.now(UTC), completed_at=None, deleted_at=None, idempotency_key=idempotency_key, request_hash=request_hash, ) self.rows[row.id] = row return SimpleNamespace(row=row) async def _run_prepared( self, prepared, *, on_delta=None, text_item_id, ): self.run_count += 1 row = prepared.row input_text = " ".join( str(part.get("text") or "") for item in row.input_items if item.get("type") == "message" for part in item.get("content") or [] ) if input_text == "调用客户端工具": row.output_items = [ { "id": "fc_contract", "type": "function_call", "status": "completed", "call_id": "call_contract", "name": "show_message", "arguments": '{"message":"请确认"}', } ] else: text = f"第 {self.run_count} 轮回复" if on_delta is not None: await on_delta(text[:3]) await on_delta(text[3:]) row.output_items = [ { "id": text_item_id, "type": "message", "status": "completed", "role": "assistant", "content": [ { "type": "output_text", "text": text, "annotations": [], } ], } ] row.status = "completed" row.completed_at = datetime.now(UTC) return row async def _get_response(self, response_id, *, api_key_id, **_kwargs): row = self.rows.get(response_id) if ( row is None or row.api_key_id != api_key_id or row.deleted_at is not None ): raise ResponsesAPIError( "Response not found", code="response_not_found", status_code=404, ) return row async def _get_idempotent_response( self, *, api_key_id, idempotency_key, request_hash, ): if idempotency_key is None: return None for row in self.rows.values(): if row.api_key_id != api_key_id or row.idempotency_key != idempotency_key: continue if row.request_hash != request_hash: raise ResponsesAPIError( "Idempotency-Key was already used with a different request", code="idempotency_key_conflict", param="Idempotency-Key", status_code=409, ) return row return None async def _delete_response(self, response_id, *, api_key_id): row = await self._get_response(response_id, api_key_id=api_key_id) row.deleted_at = datetime.now(UTC) return row async def test_three_turn_state_retrieve_input_items_and_delete(self): models = await self.client.models.list() self.assertEqual(models.data[0].id, "assistant:asst_contract") first = await self.client.responses.create( model="assistant:asst_contract", input="第一轮", ) second = await self.client.responses.create( previous_response_id=first.id, input="第二轮", ) third = await self.client.responses.create( previous_response_id=second.id, input="第三轮", ) self.assertEqual(third.previous_response_id, second.id) self.assertEqual(third.model, "assistant:asst_contract") self.assertIn("第 3 轮回复", third.output_text) retrieved = await self.client.responses.retrieve(third.id) self.assertEqual(retrieved.id, third.id) self.assertEqual(retrieved.output_text, third.output_text) raw = await self.http_client.get( f"/v1/responses/{third.id}", headers={ "Authorization": "Bearer sk-rt-key-a", "X-Request-ID": "req_contract_caller", }, ) self.assertEqual(raw.headers["x-request-id"], "req_contract_caller") page = await self.client.responses.input_items.list(third.id, order="asc") self.assertEqual(page.data[0].type, "message") self.assertTrue(page.data[0].id.startswith("msg_")) deleted = await self.client.responses.delete(first.id) # openai-python 2.x deliberately exposes this operation as returning None, # even though the HTTP endpoint returns the standard deletion object. self.assertIsNone(deleted) with self.assertRaises(NotFoundError): await self.client.responses.retrieve(first.id) async def test_sse_events_are_parsed_by_official_sdk(self): request = { "model": "assistant:asst_contract", "input": "流式输出", "stream": True, } headers = {"Idempotency-Key": "stream-contract-key"} stream = await self.client.responses.create( **request, extra_headers=headers, ) events = [event async for event in stream] replay_stream = await self.client.responses.create( **request, extra_headers=headers, ) replay_events = [event async for event in replay_stream] event_types = [event.type for event in events] self.assertEqual(self.run_count, 1) self.assertEqual( replay_events[-1].response.id, events[-1].response.id, ) self.assertEqual( event_types[0:2], ["response.created", "response.in_progress"], ) self.assertIn("response.output_text.delta", event_types) self.assertEqual(event_types[-1], "response.completed") self.assertEqual( [event.sequence_number for event in events], list(range(len(events))), ) self.assertEqual( [event.sequence_number for event in replay_events], list(range(len(replay_events))), ) retrieved = await self.client.responses.retrieve( events[-1].response.id ) self.assertEqual(retrieved.output_text, events[-1].response.output_text) async def test_client_tool_output_continues_with_sdk_types(self): tool_stream = await self.client.responses.create( model="assistant:asst_contract", input="调用客户端工具", stream=True, ) events = [event async for event in tool_stream] self.assertIn( "response.function_call_arguments.done", [event.type for event in events], ) tool_response = events[-1].response function_call = tool_response.output[0] self.assertEqual(function_call.type, "function_call") continued = await self.client.responses.create( previous_response_id=tool_response.id, input=[ { "type": "function_call_output", "call_id": function_call.call_id, "output": '{"status":"ok"}', } ], ) self.assertEqual(continued.status, "completed") page = await self.client.responses.input_items.list( continued.id, order="asc", ) self.assertEqual(page.data[0].type, "function_call_output") async def test_idempotency_unsupported_field_and_cross_key_hiding(self): headers = {"Idempotency-Key": "contract-key"} first = await self.client.responses.create( model="assistant:asst_contract", input="幂等请求", extra_headers=headers, ) replayed = await self.client.responses.create( model="assistant:asst_contract", input="幂等请求", extra_headers=headers, ) self.assertEqual(replayed.id, first.id) self.assertEqual(self.run_count, 1) with self.assertRaises(BadRequestError): await self.client.responses.cancel(first.id) malformed = await self.http_client.post( "/v1/responses", headers={"Authorization": "Bearer sk-rt-key-a"}, json={"model": "assistant:asst_contract"}, ) self.assertEqual(malformed.status_code, 422) self.assertEqual(malformed.json()["error"]["type"], "invalid_request_error") self.assertTrue(malformed.headers["x-request-id"].startswith("req_")) caller_owned_item_id = await self.http_client.post( "/v1/responses", headers={"Authorization": "Bearer sk-rt-key-a"}, json={ "model": "assistant:asst_contract", "input": [ { "type": "message", "id": "caller_owned_id", "role": "user", "content": "hello", } ], }, ) self.assertEqual(caller_owned_item_id.status_code, 422) unauthenticated = await self.http_client.get("/v1/models") self.assertEqual(unauthenticated.status_code, 401) self.assertEqual( unauthenticated.json()["error"]["code"], "invalid_api_key", ) with self.assertRaises(ConflictError): await self.client.responses.create( model="assistant:asst_contract", input="不同请求", extra_headers=headers, ) with self.assertRaises(BadRequestError): await self.client.responses.create( model="assistant:asst_contract", input="不支持", extra_body={"background": True}, ) other_http_client = httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://contract.test", ) other = AsyncOpenAI( api_key="sk-rt-key-b", base_url="http://contract.test/v1", http_client=other_http_client, max_retries=0, ) try: with self.assertRaises(NotFoundError): await other.responses.retrieve(first.id) finally: await other.close() if __name__ == "__main__": unittest.main()