feat: add gateway client with env-driven assembly

Includes config aggregation for multi-source env keys, from_env and
from_settings factories with explicit shared-backend injection,
gather_bounded, top-level exports, tightened import-linter layers with
the gate removed from the Makefile, and the finalized .env.example.
This commit is contained in:
2026-07-20 07:47:05 -04:00
parent 936895919c
commit 7b9815f4bc
30 changed files with 1701 additions and 253 deletions
+109 -42
View File
@@ -15,37 +15,80 @@ from polygateway.types import ChatRequest, LLMResponse, SourceConfig
_REQ = ChatRequest(messages=[{"role": "user", "content": "hi"}], session_id="sess-1")
_EXPECTED_COLUMNS = [
"call_id", "parent_call_id", "session_id", "model", "provider", "source_name",
"messages", "response", "thinking", "prompt_tokens", "completion_tokens",
"usage_source", "latency_ms", "ttft_ms", "max_inter_token_ms", "cache_hit",
"error", "cost", "created_at",
"call_id",
"parent_call_id",
"session_id",
"model",
"provider",
"source_name",
"messages",
"response",
"thinking",
"prompt_tokens",
"completion_tokens",
"usage_source",
"latency_ms",
"ttft_ms",
"max_inter_token_ms",
"cache_hit",
"error",
"cost",
"created_at",
]
def _resp(**overrides):
base = dict(
content="ok", thinking="", model="m", provider="p", prompt_tokens=1,
completion_tokens=2, latency_ms=30, ttft_ms=None, max_inter_token_ms=None,
cache_hit=False, call_id="cid-1", source_name="s1", usage_source="measured",
)
base = {
"content": "ok",
"thinking": "",
"model": "m",
"provider": "p",
"prompt_tokens": 1,
"completion_tokens": 2,
"latency_ms": 30,
"ttft_ms": None,
"max_inter_token_ms": None,
"cache_hit": False,
"call_id": "cid-1",
"source_name": "s1",
"usage_source": "measured",
}
base.update(overrides)
return LLMResponse(**base)
def _source():
return SourceConfig(
name="s1", provider="p", base_url="https://gw.example/v1",
api_key="sk", model="m", timeout_s=10.0,
name="s1",
provider="p",
base_url="https://gw.example/v1",
api_key="sk",
model="m",
timeout_s=10.0,
)
async def _record_minimal(recorder, call_id="c1", **overrides):
fields = dict(
call_id=call_id, parent_call_id=None, session_id="sess-1", model="m",
provider="p", source_name="s1", messages="[]", response="ok", thinking="",
prompt_tokens=1, completion_tokens=2, usage_source="measured", latency_ms=10,
ttft_ms=None, max_inter_token_ms=None, cache_hit=False, error=None, cost=None,
)
fields = {
"call_id": call_id,
"parent_call_id": None,
"session_id": "sess-1",
"model": "m",
"provider": "p",
"source_name": "s1",
"messages": "[]",
"response": "ok",
"thinking": "",
"prompt_tokens": 1,
"completion_tokens": 2,
"usage_source": "measured",
"latency_ms": 10,
"ttft_ms": None,
"max_inter_token_ms": None,
"cache_hit": False,
"error": None,
"cost": None,
}
fields.update(overrides)
await recorder.record_llm_call(**fields)
@@ -55,9 +98,9 @@ class TestSQLiteRecorder:
recorder = SQLiteRecorder(tmp_path / "t.db")
await _record_minimal(recorder)
recorder.close()
cols = [r[1] for r in sqlite3.connect(tmp_path / "t.db").execute(
"PRAGMA table_info(llm_calls)"
)]
cols = [
r[1] for r in sqlite3.connect(tmp_path / "t.db").execute("PRAGMA table_info(llm_calls)")
]
assert cols == _EXPECTED_COLUMNS
async def test_call_id_idempotent(self, tmp_path):
@@ -65,18 +108,20 @@ class TestSQLiteRecorder:
await _record_minimal(recorder, call_id="dup")
await _record_minimal(recorder, call_id="dup", response="second")
recorder.close()
rows = sqlite3.connect(tmp_path / "t.db").execute(
"SELECT response FROM llm_calls WHERE call_id='dup'"
).fetchall()
rows = (
sqlite3.connect(tmp_path / "t.db")
.execute("SELECT response FROM llm_calls WHERE call_id='dup'")
.fetchall()
)
assert rows == [("ok",)] # INSERT OR IGNORE: 第二次静默忽略
async def test_concurrent_writes_all_land(self, tmp_path):
recorder = SQLiteRecorder(tmp_path / "t.db")
await asyncio.gather(*(_record_minimal(recorder, call_id=f"c{i}") for i in range(50)))
recorder.close()
(count,) = sqlite3.connect(tmp_path / "t.db").execute(
"SELECT COUNT(*) FROM llm_calls"
).fetchone()
(count,) = (
sqlite3.connect(tmp_path / "t.db").execute("SELECT COUNT(*) FROM llm_calls").fetchone()
)
assert count == 50
async def test_unwritable_path_degrades_silently(self):
@@ -98,8 +143,12 @@ class TestEmitter:
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec)
await emitter.emit_attempt(
request=_REQ, source=_source(), call_id="cid-1", latency_ms=42,
response=_resp(), error=None,
request=_REQ,
source=_source(),
call_id="cid-1",
latency_ms=42,
response=_resp(),
error=None,
)
row = rec.rows[0]
assert row["call_id"] == "cid-1" and row["error"] is None
@@ -110,8 +159,12 @@ class TestEmitter:
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec)
await emitter.emit_attempt(
request=_REQ, source=_source(), call_id="cid-2", latency_ms=7,
response=None, error="TransientError: boom",
request=_REQ,
source=_source(),
call_id="cid-2",
latency_ms=7,
response=None,
error="TransientError: boom",
)
row = rec.rows[0]
assert row["error"].startswith("TransientError")
@@ -121,12 +174,23 @@ class TestEmitter:
rec = _MemoryRecorder()
emitter = TelemetryEmitter(rec)
big = "data:image/png;base64," + "A" * 100_000
req = ChatRequest(messages=[{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": big}},
]}])
req = ChatRequest(
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": big}},
],
}
]
)
await emitter.emit_attempt(
request=req, source=_source(), call_id="c", latency_ms=1,
response=None, error="x",
request=req,
source=_source(),
call_id="c",
latency_ms=1,
response=None,
error="x",
)
assert len(rec.rows[0]["messages"]) < 500 # base64 不整段进库(VT R12)
@@ -137,8 +201,12 @@ class TestEmitter:
emitter = TelemetryEmitter(Broken())
await emitter.emit_attempt(
request=_REQ, source=_source(), call_id="c", latency_ms=1,
response=_resp(), error=None,
request=_REQ,
source=_source(),
call_id="c",
latency_ms=1,
response=_resp(),
error=None,
) # 不抛(降级不冒泡)
@@ -194,10 +262,9 @@ def test_single_emitter_discipline():
"""铁律执法: record_llm_call 在 src/ 的调用点只允许出现在 telemetry emitter。"""
out = subprocess.run(
["grep", "-rln", "record_llm_call(", "src/polygateway"],
capture_output=True, text=True, cwd=Path(__file__).resolve().parents[2],
capture_output=True,
text=True,
cwd=Path(__file__).resolve().parents[2],
).stdout.splitlines()
callers = [
p for p in out
if not p.endswith(("ports.py", "telemetry/sqlite.py"))
]
callers = [p for p in out if not p.endswith(("ports.py", "telemetry/sqlite.py"))]
assert callers == ["src/polygateway/middleware/telemetry.py"]