test_ollama.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """
  2. Tests for Ollama (local / self-hosted) provider support.
  3. Two groups:
  4. 1. Cost accounting — pure unit tests, no Ollama needed. Verify local models
  5. are billed at $0 and the cost source label reflects the real provider.
  6. 2. Live provider — skipped automatically unless an Ollama server is reachable
  7. at http://127.0.0.1:11434. Run a local model first, e.g.:
  8. ollama pull qwen2.5:7b && ollama serve
  9. """
  10. import urllib.request
  11. import pytest
  12. from agentpaas.engine.sandbox import _compute_cost_usd
  13. # ============================================================
  14. # 1. Cost accounting (no Ollama required)
  15. # ============================================================
  16. class TestOllamaCost:
  17. def test_ollama_costs_zero(self):
  18. """Local Ollama models incur no per-token API cost."""
  19. usage = {"input_tokens": 10_000, "output_tokens": 5_000, "provider": "ollama"}
  20. assert _compute_cost_usd(usage) == 0.0
  21. def test_claude_still_billed(self):
  22. """Regression: non-local providers keep their pricing."""
  23. usage = {"input_tokens": 1_000_000, "output_tokens": 0, "provider": "claude"}
  24. assert _compute_cost_usd(usage) == pytest.approx(3.00)
  25. def test_qwen_still_billed(self):
  26. usage = {"input_tokens": 1_000_000, "output_tokens": 0, "provider": "qwen"}
  27. assert _compute_cost_usd(usage) == pytest.approx(5.56)
  28. # ============================================================
  29. # 2. Live provider (skipped unless Ollama is up)
  30. # ============================================================
  31. _OLLAMA_URL = "http://127.0.0.1:11434/api/tags"
  32. def _ollama_models():
  33. """Return the list of locally available Ollama model names, or [] if down."""
  34. try:
  35. with urllib.request.urlopen(_OLLAMA_URL, timeout=3) as resp:
  36. import json
  37. data = json.loads(resp.read())
  38. return [m["name"] for m in data.get("models", [])]
  39. except Exception:
  40. return []
  41. _MODELS = _ollama_models()
  42. _HAS_OLLAMA = bool(_MODELS)
  43. # Prefer a small model if present, else use whatever is available.
  44. _MODEL = next((m for m in _MODELS if "7b" in m or "3b" in m), _MODELS[0] if _MODELS else "qwen2.5:7b")
  45. ollama_required = pytest.mark.skipif(
  46. not _HAS_OLLAMA, reason="No Ollama server reachable at 127.0.0.1:11434"
  47. )
  48. @ollama_required
  49. class TestOllamaLive:
  50. def test_chat_roundtrip(self):
  51. from lambdagent.providers import create_provider
  52. p = create_provider("ollama", model=_MODEL, timeout=120)
  53. assert p.provider_name == "ollama"
  54. assert p.base_url.startswith("http://127.0.0.1:11434")
  55. out = p.chat([{"role": "user", "content": "Reply with exactly: OLLAMA_OK"}])
  56. assert "OLLAMA_OK" in out
  57. def test_usage_accumulates(self):
  58. from lambdagent.providers import create_provider, ChatMessage
  59. p = create_provider("ollama", model=_MODEL, timeout=120)
  60. r = p.chat_typed([ChatMessage(role="user", content="Say hi.")], max_tokens=16)
  61. assert r.text
  62. assert r.input_tokens > 0
  63. usage = p.get_usage()
  64. assert usage["provider"] == "ollama"
  65. assert usage["input_tokens"] > 0
  66. def test_full_stack_run_costs_zero(self):
  67. """A simple agent run on Ollama produces artifacts and bills $0."""
  68. import asyncio, tempfile, os, json, shutil
  69. from agentpaas.engine.sandbox import Sandbox
  70. config = {
  71. "name": "ollama-smoke", "type": "simple",
  72. "systemPrompt": "You are concise.",
  73. "model": {"provider": "ollama", "name": _MODEL,
  74. "temperature": 0.0, "maxTokens": 64},
  75. }
  76. agent_dir = tempfile.mkdtemp(prefix="ollama_test_")
  77. try:
  78. sb = Sandbox(level=0)
  79. r = asyncio.run(sb.execute(
  80. config, "Reply with exactly: FULLSTACK_OK",
  81. timeout=120, agent_dir=agent_dir, run_id="run_olltest",
  82. ))
  83. assert r.status == "completed", r.error
  84. ws = r.workspace_path
  85. for fn in ("output.json", "cost.json", "manifest.json"):
  86. assert os.path.isfile(os.path.join(ws, fn))
  87. cost = json.load(open(os.path.join(ws, "cost.json")))
  88. assert cost["cost_usd"] == 0.0
  89. finally:
  90. shutil.rmtree(agent_dir, ignore_errors=True)