| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- """
- Tests for Ollama (local / self-hosted) provider support.
- Two groups:
- 1. Cost accounting — pure unit tests, no Ollama needed. Verify local models
- are billed at $0 and the cost source label reflects the real provider.
- 2. Live provider — skipped automatically unless an Ollama server is reachable
- at http://127.0.0.1:11434. Run a local model first, e.g.:
- ollama pull qwen2.5:7b && ollama serve
- """
- import urllib.request
- import pytest
- from agentpaas.engine.sandbox import _compute_cost_usd
- # ============================================================
- # 1. Cost accounting (no Ollama required)
- # ============================================================
- class TestOllamaCost:
- def test_ollama_costs_zero(self):
- """Local Ollama models incur no per-token API cost."""
- usage = {"input_tokens": 10_000, "output_tokens": 5_000, "provider": "ollama"}
- assert _compute_cost_usd(usage) == 0.0
- def test_claude_still_billed(self):
- """Regression: non-local providers keep their pricing."""
- usage = {"input_tokens": 1_000_000, "output_tokens": 0, "provider": "claude"}
- assert _compute_cost_usd(usage) == pytest.approx(3.00)
- def test_qwen_still_billed(self):
- usage = {"input_tokens": 1_000_000, "output_tokens": 0, "provider": "qwen"}
- assert _compute_cost_usd(usage) == pytest.approx(5.56)
- # ============================================================
- # 2. Live provider (skipped unless Ollama is up)
- # ============================================================
- _OLLAMA_URL = "http://127.0.0.1:11434/api/tags"
- def _ollama_models():
- """Return the list of locally available Ollama model names, or [] if down."""
- try:
- with urllib.request.urlopen(_OLLAMA_URL, timeout=3) as resp:
- import json
- data = json.loads(resp.read())
- return [m["name"] for m in data.get("models", [])]
- except Exception:
- return []
- _MODELS = _ollama_models()
- _HAS_OLLAMA = bool(_MODELS)
- # Prefer a small model if present, else use whatever is available.
- _MODEL = next((m for m in _MODELS if "7b" in m or "3b" in m), _MODELS[0] if _MODELS else "qwen2.5:7b")
- ollama_required = pytest.mark.skipif(
- not _HAS_OLLAMA, reason="No Ollama server reachable at 127.0.0.1:11434"
- )
- @ollama_required
- class TestOllamaLive:
- def test_chat_roundtrip(self):
- from lambdagent.providers import create_provider
- p = create_provider("ollama", model=_MODEL, timeout=120)
- assert p.provider_name == "ollama"
- assert p.base_url.startswith("http://127.0.0.1:11434")
- out = p.chat([{"role": "user", "content": "Reply with exactly: OLLAMA_OK"}])
- assert "OLLAMA_OK" in out
- def test_usage_accumulates(self):
- from lambdagent.providers import create_provider, ChatMessage
- p = create_provider("ollama", model=_MODEL, timeout=120)
- r = p.chat_typed([ChatMessage(role="user", content="Say hi.")], max_tokens=16)
- assert r.text
- assert r.input_tokens > 0
- usage = p.get_usage()
- assert usage["provider"] == "ollama"
- assert usage["input_tokens"] > 0
- def test_full_stack_run_costs_zero(self):
- """A simple agent run on Ollama produces artifacts and bills $0."""
- import asyncio, tempfile, os, json, shutil
- from agentpaas.engine.sandbox import Sandbox
- config = {
- "name": "ollama-smoke", "type": "simple",
- "systemPrompt": "You are concise.",
- "model": {"provider": "ollama", "name": _MODEL,
- "temperature": 0.0, "maxTokens": 64},
- }
- agent_dir = tempfile.mkdtemp(prefix="ollama_test_")
- try:
- sb = Sandbox(level=0)
- r = asyncio.run(sb.execute(
- config, "Reply with exactly: FULLSTACK_OK",
- timeout=120, agent_dir=agent_dir, run_id="run_olltest",
- ))
- assert r.status == "completed", r.error
- ws = r.workspace_path
- for fn in ("output.json", "cost.json", "manifest.json"):
- assert os.path.isfile(os.path.join(ws, fn))
- cost = json.load(open(os.path.join(ws, "cost.json")))
- assert cost["cost_usd"] == 0.0
- finally:
- shutil.rmtree(agent_dir, ignore_errors=True)
|