From 7ca82338890e3000659d0bd177339d8d3b822bf3 Mon Sep 17 00:00:00 2001 From: Derek Higgins Date: Tue, 26 Aug 2025 17:17:00 +0100 Subject: [PATCH] feat(testing): remove SQLite dependency from inference recorder (#3254) Recording files use a predictable naming format, making the SQLite index redundant. The binary SQLite file was causing frequent git conflicts. Simplify by calculating file paths directly from request hashes. Signed-off-by: Derek Higgins --- llama_stack/testing/inference_recorder.py | 43 +----------------- tests/integration/recordings/index.sqlite | Bin 57344 -> 0 bytes .../distribution/test_inference_recordings.py | 16 +------ 3 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 tests/integration/recordings/index.sqlite diff --git a/llama_stack/testing/inference_recorder.py b/llama_stack/testing/inference_recorder.py index 4a6958399..8fa5f5f2e 100644 --- a/llama_stack/testing/inference_recorder.py +++ b/llama_stack/testing/inference_recorder.py @@ -9,7 +9,6 @@ from __future__ import annotations # for forward references import hashlib import json import os -import sqlite3 from collections.abc import Generator from contextlib import contextmanager from enum import StrEnum @@ -125,28 +124,13 @@ class ResponseStorage: def __init__(self, test_dir: Path): self.test_dir = test_dir self.responses_dir = self.test_dir / "responses" - self.db_path = self.test_dir / "index.sqlite" self._ensure_directories() - self._init_database() def _ensure_directories(self): self.test_dir.mkdir(parents=True, exist_ok=True) self.responses_dir.mkdir(exist_ok=True) - def _init_database(self): - with sqlite3.connect(self.db_path) as conn: - conn.execute(""" - CREATE TABLE IF NOT EXISTS recordings ( - request_hash TEXT PRIMARY KEY, - response_file TEXT, - endpoint TEXT, - model TEXT, - timestamp TEXT, - is_streaming BOOLEAN - ) - """) - def store_recording(self, request_hash: str, request: dict[str, Any], response: dict[str, Any]): """Store a request/response pair.""" # Generate unique response filename @@ -169,34 +153,9 @@ class ResponseStorage: f.write("\n") f.flush() - # Update SQLite index - with sqlite3.connect(self.db_path) as conn: - conn.execute( - """ - INSERT OR REPLACE INTO recordings - (request_hash, response_file, endpoint, model, timestamp, is_streaming) - VALUES (?, ?, ?, ?, datetime('now'), ?) - """, - ( - request_hash, - response_file, - request.get("endpoint", ""), - request.get("model", ""), - response.get("is_streaming", False), - ), - ) - def find_recording(self, request_hash: str) -> dict[str, Any] | None: """Find a recorded response by request hash.""" - with sqlite3.connect(self.db_path) as conn: - result = conn.execute( - "SELECT response_file FROM recordings WHERE request_hash = ?", (request_hash,) - ).fetchone() - - if not result: - return None - - response_file = result[0] + response_file = f"{request_hash[:12]}.json" response_path = self.responses_dir / response_file if not response_path.exists(): diff --git a/tests/integration/recordings/index.sqlite b/tests/integration/recordings/index.sqlite deleted file mode 100644 index 0c88416f1e7c84196c1dd80877c3ff4bcd8322da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeI53y@_;dEd|6duQg(8!h4?z%UZ20+m?p>C>klXG#S~us8$(LMF05ZrSm6#&3jhzIUr}O&t zIdf+kq*=75w=7w{JKE9i&h5V6{kl*0_xOMR?yPp4ME$l?^~uefZ_Q8My8gz4hu*k;!;P~Vt&CTH>bvx#O4{)V|LX3vejIePQ>=b}5K|LKPH`*mvTrt#LwffuaEv9!k2 zWh07GCv|1BI7u2U>b%NLB5juGRFp!*wcsUn!lXxSR8;Lr#b!hUwX)g0LO-&(mVbe^RtY~yBYLUmW(phFw z)ilCnD$A`Zh0&rYY^ADHsW_`m;*Z~b-_87%laKMfTbFLV)V=XlFOv%*$%Jf@xY2o8 z%DAq|Cbnr}lT>F-rIWm>RL+Mrr7V<8id;!u7-Lit+e{>lKfZh8JH&zV_M^v-)SHLd zzDJJaM{{{~vLTE8hmWy?PL-Sco8v-zatXVU+KpHs*EQR6jf=Fbc(1aG)269nnX5Dw zrKx0*m9|lW4_w zW!q9M=(3EpEQ$&Ru{%wpR9?hJc&Ery5 zoDyZ@j}MefBiG;DmTN(ZG&XfC5hh8iOj#?^GP6cxjZ_UsQ)x0OQA`%gTFPAV(?*(# z^;5a!-0;Q+%B7I&RTF*XO(CVQNh7u5^XH{Z>ZaBW2biv!8aY!=Wg~TNW66@1d1WiM zqO95Rd8+;KfpVqn`b(T#?t~D9m08+kJdirebCJb`5SA0k9UDrfJO!+8R_nB}Qrlc= z4&Ne;3&SO&6=&`4_)ac&gdhCznlY z7B7i2⁡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 ro;MLEv}=q%zLz*MJ9Tfnqb@o=@wSt