test_model_parameters.py 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import asyncio
  2. import json
  3. import secrets
  4. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  5. from queue import SimpleQueue
  6. from threading import Thread
  7. import httpx
  8. import pytest
  9. from langchain_openai import ChatOpenAI
  10. from openai import APIConnectionError
  11. from zhifu.core.model_parameters import chat_parameters
  12. class Receiver(BaseHTTPRequestHandler):
  13. def do_POST(self):
  14. # 标准 HTTP 服务读取 SDK 实际发送的 JSON,然后关闭连接,不生成模型响应。
  15. body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
  16. self.server.received.put((self.path, body))
  17. self.close_connection = True
  18. def log_message(self, format, *args):
  19. pass
  20. @pytest.mark.parametrize("base_url,expected_field", [
  21. ("https://api.deepseek.com", "max_tokens"),
  22. ("https://api.deepseek.com/v1", "max_tokens"),
  23. ("https://api.openai.com/v1", "max_completion_tokens"),
  24. ("https://api.deepseek.com.example.org/v1", "max_completion_tokens"),
  25. ])
  26. @pytest.mark.parametrize("with_tools", [False, True])
  27. async def test_sdk_sends_bounded_parameters_to_real_local_receiver(
  28. base_url, expected_field, with_tools,
  29. ):
  30. server = ThreadingHTTPServer(("127.0.0.1", 0), Receiver)
  31. server.received = SimpleQueue()
  32. thread = Thread(target=server.serve_forever, kwargs={"poll_interval": 0.01}, daemon=True)
  33. thread.start()
  34. # 参数按供应商 URL 构建,传输仅连接本地接收器,整个测试不访问模型供应商。
  35. client = ChatOpenAI(
  36. model="deepseek-flash", base_url=f"http://127.0.0.1:{server.server_port}/v1",
  37. api_key=secrets.token_urlsafe(32), max_retries=0, timeout=2, use_responses_api=False,
  38. **chat_parameters(base_url, 256, {"thinking": {"type": "disabled"}}),
  39. http_client=httpx.Client(timeout=2, trust_env=False),
  40. http_async_client=httpx.AsyncClient(timeout=2, trust_env=False),
  41. )
  42. try:
  43. runnable = client.bind_tools([{
  44. "type": "function", "function": {
  45. "name": "get_ticket", "description": "查询当前授权工单",
  46. "parameters": {"type": "object", "properties": {"ticket_id": {
  47. "type": "string"}}, "required": ["ticket_id"]},
  48. },
  49. }]) if with_tools else client
  50. async with asyncio.timeout(5):
  51. with pytest.raises(APIConnectionError):
  52. await runnable.ainvoke("请说明当前工单状态。")
  53. assert server.received.qsize() == 1
  54. path, body = server.received.get_nowait()
  55. assert path == "/v1/chat/completions"
  56. assert body[expected_field] == 256
  57. alternative = "max_completion_tokens" if expected_field == "max_tokens" else "max_tokens"
  58. assert alternative not in body
  59. assert "max_output_tokens" not in body
  60. assert body["thinking"] == {"type": "disabled"}
  61. assert bool(body.get("tools")) == with_tools
  62. finally:
  63. await client.root_async_client.close()
  64. client.root_client.close()
  65. await asyncio.to_thread(server.shutdown)
  66. await asyncio.to_thread(thread.join, 2)
  67. server.server_close()
  68. @pytest.mark.parametrize("name", ["max_tokens", "max_completion_tokens", "max_output_tokens"])
  69. @pytest.mark.parametrize("value", [512, 0, None, True, "256"])
  70. def test_extra_parameters_cannot_override_output_limit(name, value):
  71. with pytest.raises(ValueError, match="输出上限冲突"):
  72. chat_parameters("https://api.deepseek.com/v1", 256, {name: value})
  73. def test_equal_limit_aliases_preserve_configuration_and_use_provider_field():
  74. body = {"max_completion_tokens": 256, "max_tokens": 256,
  75. "thinking": {"type": "disabled"}}
  76. assert chat_parameters("https://API.DEEPSEEK.COM/v1", 256, body) == {
  77. "extra_body": {"max_tokens": 256, "thinking": {"type": "disabled"}}}
  78. assert body["max_completion_tokens"] == 256
  79. assert chat_parameters("https://api.deepseek.com@other.example/v1", 256, body) == {
  80. "max_tokens": 256, "extra_body": {"thinking": {"type": "disabled"}}}