oc_chat
Loading...
Searching...
No Matches
m0_11_ollama_smoke.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/scripts/m0_11_ollama_smoke.py
4
5# OCT-LINT: disable=tests_exist reason="Live smoke harness — exercising it against a real Ollama daemon IS the test; no unit test applies."
6"""
7M0.11 live-smoke harness for the Ollama provider (O1 / O4 / O5).
8
9Purpose
10-------
11Exercises the Ollama provider against a real daemon and records
12pass/fail for the three deliverables that shipped with unit tests
13but were never live-verified:
14
15 O1 provider construction, model listing, a streamed turn,
16 model_info, and the clean-error path on an unreachable host
17 O4 generation options (top_k) reaching the /api/chat request body
18 O5 PerformanceStats / tok-s footer from real generation timing
19
20Run
21---
22 OLLAMA_BASE_URL=http://192.168.1.20:11434 \\
23 python -m oc_chat.scripts.m0_11_ollama_smoke [--model llama3.2:3b]
24
25Exit code 0 when every smoke passes, 1 otherwise.
26
27Responsibilities
28----------------
29- Build the provider/client exactly as OC Chat does at runtime.
30- Capture the live /api/chat request body for the O4 assertion.
31- Print one ``PASS``/``FAIL`` line per smoke plus an evidence block.
32
33Diagnostics
34-----------
35Domain: OC_CHAT.CLIENT
36Levels:
37 L2 - smoke lifecycle events
38 L3 - per-smoke evidence
39 L4 - per-call payload traces
40
41Contracts
42---------
43- Read-only against the daemon apart from normal generation calls.
44- Never mutates repo state; prints a record to stdout only.
45
46Dependencies
47------------
48- oc_diagnostics (_dbg)
49- oc_chat.core.generation_options (lazy; GenerationOptions for the O4 top_k smoke)
50- providers.ollama.provider (lazy; OllamaProvider, the provider under test: client build, model_info, generation-option translation and performance parsing)
51- providers.ollama.client (lazy; _post, patched to capture the live /api/chat request body)
52"""
53from __future__ import annotations
54
55import argparse
56import os
57import sys
58from typing import Any
59
60from oc_diagnostics import _dbg
61
62DEAD_URL = "http://localhost:11500" # nothing listens here -> instant refuse
63
64
65def _smoke_o1(base_url: str, model: str) -> tuple[bool, list[str]]:
66 """O1 — provider build, list_models, streamed turn, model_info, clean error."""
67 func = "_smoke_o1"
68 ev: list[str] = []
69 ok = True
70 from providers.ollama.provider import OllamaProvider
71
72 provider = OllamaProvider()
73 client = provider.build_client(api_key="", base_url=base_url)
74 ev.append(f"O1a provider/client built — base_url={client.base_url!r}")
75
76 models = client.list_models()
77 has_model = model in models
78 ok &= has_model
79 ev.append(f"O1b list_models -> {len(models)} tags; {model!r} present: {has_model}")
80
81 chunks: list[str] = []
82 usage: dict[str, Any] = {}
83 for kind, data in client.stream_chat(
84 [{"role": "user", "content": "Reply with exactly: smoke ok"}],
85 model,
86 max_tokens=32,
87 ):
88 if kind == "content":
89 chunks.append(data)
90 elif kind == "usage":
91 usage = data
92 text = "".join(chunks).strip()
93 streamed_ok = len(text) > 0
94 ok &= streamed_ok
95 ev.append(f"O1c streamed turn -> {len(text)} chars: {text[:60]!r}")
96
97 info = provider.model_info(model, client)
98 info_ok = info is not None
99 ok &= info_ok
100 if info is not None:
101 ev.append(f"O1d model_info -> family={info.family!r} params={info.parameter_count!r}")
102 else:
103 ev.append("O1d model_info -> None (FAIL)")
104
105 dead = provider.build_client(api_key="", base_url=DEAD_URL)
106 try:
107 dead.list_models()
108 ev.append("O1e clean-error path -> no error raised (FAIL)")
109 ok = False
110 except RuntimeError as exc:
111 friendly = "unreachable" in str(exc).lower()
112 ok &= friendly
113 ev.append(f"O1e unreachable host -> clean RuntimeError, friendly={friendly}: {str(exc)[:80]!r}")
114 except Exception as exc: # noqa: BLE001 - smoke must report any non-clean failure
115 ev.append(f"O1e unreachable host -> non-clean {type(exc).__name__} (FAIL)")
116 ok = False
117
118 for line in ev:
119 _dbg("OC_CHAT.CLIENT", func, line, 3)
120 _dbg("OC_CHAT.CLIENT", func, f"O1 ok={ok}", 2)
121 return ok, ev
122
123
124def _smoke_o4(base_url: str, model: str) -> tuple[bool, list[str]]:
125 """O4 — generation options (top_k) reach the live /api/chat request body."""
126 func = "_smoke_o4"
127 ev: list[str] = []
128 from oc_chat.core.generation_options import GenerationOptions
129 from providers.ollama.provider import OllamaProvider
130 from providers.ollama import client as ollama_client
131
132 provider = OllamaProvider()
133 extra_body = provider.translate_generation_options(GenerationOptions(top_k=10))
134 ev.append(f"O4a translate_generation_options(top_k=10) -> {extra_body}")
135
136 captured: dict[str, Any] = {}
137 original_post = ollama_client._post
138
139 def _capturing_post(b_url: str, path: str, body: dict, timeout: int = 300) -> Any:
140 if path == "/api/chat":
141 captured.update(body)
142 return original_post(b_url, path, body, timeout)
143
144 ollama_client._post = _capturing_post # type: ignore[assignment]
145 try:
146 cli = provider.build_client(api_key="", base_url=base_url)
147 for _kind, _data in cli.stream_chat(
148 [{"role": "user", "content": "hi"}], model,
149 extra_body=extra_body, max_tokens=8,
150 ):
151 pass
152 finally:
153 ollama_client._post = original_post # type: ignore[assignment]
154
155 sent_top_k = (captured.get("options") or {}).get("top_k")
156 ok = sent_top_k == 10
157 ev.append(f"O4b live /api/chat body options={captured.get('options')!r} -> top_k={sent_top_k}")
158 for line in ev:
159 _dbg("OC_CHAT.CLIENT", func, line, 3)
160 _dbg("OC_CHAT.CLIENT", func, f"O4 ok={ok}", 2)
161 return ok, ev
162
163
164def _smoke_o5(base_url: str, model: str) -> tuple[bool, list[str]]:
165 """O5 — PerformanceStats / tok-s footer from real generation timing."""
166 func = "_smoke_o5"
167 ev: list[str] = []
168 from providers.ollama.provider import OllamaProvider
169
170 provider = OllamaProvider()
171 client = provider.build_client(api_key="", base_url=base_url)
172
173 usage: dict[str, Any] = {}
174 for kind, data in client.stream_chat(
175 [{"role": "user", "content": "Count to five."}], model, max_tokens=64,
176 ):
177 if kind == "usage":
178 usage = data
179 perf = provider.parse_performance(usage)
180 ev.append(
181 f"O5a parse_performance -> eval_count={perf.eval_count} "
182 f"eval_duration_ns={perf.eval_duration_ns} is_default={perf.is_default()}"
183 )
184
185 tps = perf.tokens_per_second
186 ok = (not perf.is_default()) and tps is not None and tps > 0
187 if tps is not None:
188 footer = f"{perf.eval_count}tok@{tps:.1f}tok/s"
189 ev.append(f"O5b tok/s footer -> {footer!r}")
190 else:
191 ev.append("O5b tokens_per_second -> None (FAIL)")
192 for line in ev:
193 _dbg("OC_CHAT.CLIENT", func, line, 3)
194 _dbg("OC_CHAT.CLIENT", func, f"O5 ok={ok}", 2)
195 return ok, ev
196
197
198def main(argv: list[str] | None = None) -> int:
199 """Run the O1/O4/O5 smokes; return 0 when all pass."""
200 func = "main"
201 parser = argparse.ArgumentParser(description="M0.11 Ollama live-smoke harness.")
202 parser.add_argument("--model", default="llama3.2:3b", help="Ollama model tag to smoke")
203 args = parser.parse_args(argv)
204
205 base_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
206 print(f"=== M0.11 Ollama live smoke — base_url={base_url} model={args.model} ===\n")
207
208 results: list[tuple[str, bool, list[str]]] = []
209 for name, fn in (("O1", _smoke_o1), ("O4", _smoke_o4), ("O5", _smoke_o5)):
210 try:
211 ok, ev = fn(base_url, args.model)
212 except Exception as exc: # noqa: BLE001 - a crashed smoke is a FAIL, not a traceback
213 ok, ev = False, [f"{name} raised {type(exc).__name__}: {exc}"]
214 results.append((name, ok, ev))
215 print(f"[{'PASS' if ok else 'FAIL'}] {name}")
216 for line in ev:
217 print(f" {line}")
218 print()
219
220 all_ok = all(ok for _name, ok, _ev in results)
221 print(f"=== {'ALL SMOKES PASS' if all_ok else 'SMOKE FAILURES PRESENT'} ===")
222 _dbg("OC_CHAT.CLIENT", func, f"smoke complete all_ok={all_ok}", 2)
223 return 0 if all_ok else 1
224
225
226if __name__ == "__main__":
227 sys.exit(main())
tuple[bool, list[str]] _smoke_o5(str base_url, str model)
tuple[bool, list[str]] _smoke_o4(str base_url, str model)
tuple[bool, list[str]] _smoke_o1(str base_url, str model)