provider_timeout_model_acceptance.py 4.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import asyncio
  2. import json
  3. from pathlib import Path
  4. from time import perf_counter
  5. from uuid import UUID, uuid4
  6. import pytest
  7. from openai import APITimeoutError
  8. from sqlalchemy import select
  9. from test_after_sales import scenario # noqa: F401
  10. from test_money_budget import PRICE, approve, budget, setup
  11. from zhifu.modules.agent_runtime.budget_callback import CallAllowance
  12. from zhifu.modules.agent_runtime.config import AgentSettings
  13. from zhifu.modules.agent_runtime.models import AgentRun, ModelAttempt, TokenEntry
  14. from zhifu.modules.agent_runtime.worker import execute_one
  15. from zhifu.modules.billing.models import MoneyEntry
  16. def reserve_sample():
  17. with Path(".work/provider-timeout-recheck-allowance-20260922.json").open(
  18. "x", encoding="utf-8"
  19. ) as file:
  20. json.dump({"request_limit": 1, "output_limit": 128, "sdk_retries": 0}, file)
  21. async def test_actual_timeout_keeps_unknown_token_and_money(scenario): # noqa: F811
  22. settings = AgentSettings(max_output_tokens=128, model_timeout=30)
  23. sessions, state = scenario["app"].state.sessions, scenario["app"].state.state
  24. await setup(scenario)
  25. await budget(scenario, 1000000)
  26. await approve(scenario, {**PRICE, "model": settings.model_name, "provider": settings.model_url})
  27. key = str(uuid4())
  28. body = {"store_id": str(scenario["store"]), "message": "你好,请简短介绍售后服务。"}
  29. queued = await scenario["call"]("POST", "/openapi/v1/agent/runs", payload=body, key=key)
  30. assert queued.status_code == 202, queued.text
  31. run_id = UUID(queued.json()["run_id"]).bytes
  32. async with sessions.begin() as session:
  33. run = await session.get(AgentRun, run_id)
  34. run.release_snapshot = {**run.release_snapshot, "max_output_tokens": 128,
  35. "model_timeout": 0.25}
  36. await asyncio.to_thread(reserve_sample)
  37. allowance, started = CallAllowance(1), perf_counter()
  38. report = {"status": "INCOMPLETE", "request_limit": 1, "max_output_tokens": 128,
  39. "sdk_retries": 0, "request_timeout_seconds": 0.25, "total_timeout_seconds": 90}
  40. try:
  41. async with asyncio.timeout(90):
  42. with pytest.raises((TimeoutError, APITimeoutError)) as failure:
  43. await execute_one(sessions, state, settings, allowance=allowance, run_ids=[run_id])
  44. repeated = await scenario["call"](
  45. "POST", "/openapi/v1/agent/runs", payload=body, key=key)
  46. assert repeated.json()["run_id"] == str(UUID(bytes=run_id))
  47. assert not await execute_one(
  48. sessions, state, settings, allowance=allowance, run_ids=[run_id])
  49. async with sessions() as session:
  50. run = await session.get(AgentRun, run_id)
  51. attempts = (await session.scalars(select(ModelAttempt).where(
  52. ModelAttempt.run_id == run_id))).all()
  53. entries = (await session.scalars(select(TokenEntry).where(
  54. TokenEntry.tenant_id == scenario["tenant"].bytes))).all()
  55. money = (await session.scalars(select(MoneyEntry).where(
  56. MoneyEntry.tenant_id == scenario["tenant"].bytes))).all()
  57. report.update({
  58. "run_id": str(UUID(bytes=run_id)), "run_status": run.status,
  59. "error": type(failure.value).__name__, "supplier_delivery": "UNKNOWN",
  60. "attempts": [{"status": row.status, "usage": row.usage} for row in attempts],
  61. "token_entries": [{"status": row.status, "reserved": row.reserved,
  62. "usage": row.usage} for row in entries],
  63. "money_entries": [{"status": row.status, "reserved": row.reserved}
  64. for row in money],
  65. "price_basis": "真实审批的事务测试价格,不作为供应商实际费用",
  66. })
  67. assert run.status == "FAILED"
  68. assert len(attempts) == len(entries) == len(money) == allowance.started == 1
  69. assert attempts[0].status == entries[0].status == money[0].status == "UNKNOWN"
  70. assert entries[0].usage is None and entries[0].reserved > 0
  71. assert money[0].reserved > 0
  72. report["status"] = "PASSED"
  73. finally:
  74. report["actual_attempts"] = allowance.started
  75. report["elapsed_seconds"] = round(perf_counter() - started, 3)
  76. await asyncio.to_thread(Path(".work/provider-timeout-recheck-20260922.json").write_text,
  77. json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")