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:
+77
-25
@@ -31,48 +31,89 @@ class _DummyPermit:
|
||||
|
||||
|
||||
class _DummyLimiter:
|
||||
async def try_acquire(self, source_key: str, est_tokens: int): return _DummyPermit()
|
||||
async def acquire(self, source_key: str, est_tokens: int): return _DummyPermit()
|
||||
async def source_stats(self, source_key: str): return SourceStats(0, 0, 0)
|
||||
async def try_acquire(self, source_key: str, est_tokens: int):
|
||||
return _DummyPermit()
|
||||
|
||||
async def acquire(self, source_key: str, est_tokens: int):
|
||||
return _DummyPermit()
|
||||
|
||||
async def source_stats(self, source_key: str):
|
||||
return SourceStats(0, 0, 0)
|
||||
|
||||
async def mark_progress(self) -> None: ...
|
||||
async def progress_age_s(self) -> float: return 0.0
|
||||
async def progress_age_s(self) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
class _DummyGate:
|
||||
async def try_enter(self, source_name: str, owner: str): raise NotImplementedError
|
||||
async def record_success(self, entry): raise NotImplementedError
|
||||
async def record_failure(self, entry, reason: str, force_open: bool): raise NotImplementedError
|
||||
async def release_probe(self, entry): raise NotImplementedError
|
||||
async def retry_after_s(self, sources): return 0.0
|
||||
async def try_enter(self, source_name: str, owner: str):
|
||||
raise NotImplementedError
|
||||
|
||||
async def record_success(self, entry):
|
||||
raise NotImplementedError
|
||||
|
||||
async def record_failure(self, entry, reason: str, force_open: bool):
|
||||
raise NotImplementedError
|
||||
|
||||
async def release_probe(self, entry):
|
||||
raise NotImplementedError
|
||||
|
||||
async def retry_after_s(self, sources):
|
||||
return 0.0
|
||||
|
||||
|
||||
class _DummyMw:
|
||||
async def __call__(self, request, call_next): return await call_next(request)
|
||||
async def __call__(self, request, call_next):
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
class _DummyTransport:
|
||||
async def complete(self, *, messages, source, stream, overlay, call_id): raise NotImplementedError
|
||||
async def complete(self, *, messages, source, stream, overlay, call_id):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _DummyCache:
|
||||
async def get(self, key: str): return None
|
||||
async def get(self, key: str):
|
||||
return None
|
||||
|
||||
async def set(self, key: str, value: str, ttl_s: int) -> None: ...
|
||||
|
||||
|
||||
class _DummySelector:
|
||||
def order(self, sources, stats): return list(sources)
|
||||
def order(self, sources, stats):
|
||||
return list(sources)
|
||||
|
||||
|
||||
class _DummyStrategy:
|
||||
def request_overlay(self, schema): return {}
|
||||
def parse(self, text: str) -> Any: return {}
|
||||
def request_overlay(self, schema):
|
||||
return {}
|
||||
|
||||
def parse(self, text: str) -> Any:
|
||||
return {}
|
||||
|
||||
|
||||
class _DummyRecorder:
|
||||
async def record_llm_call(
|
||||
self, *, 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,
|
||||
self,
|
||||
*,
|
||||
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,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@@ -95,10 +136,15 @@ def test_protocols_are_runtime_checkable(impl, protocol):
|
||||
|
||||
|
||||
def _decision(**overrides) -> GateDecision:
|
||||
base = dict(
|
||||
source_name="qwen_1", allowed=True, state=GateState.CLOSED,
|
||||
epoch=0, is_probe=False, probe_owner=None, retry_after_s=0.0,
|
||||
)
|
||||
base = {
|
||||
"source_name": "qwen_1",
|
||||
"allowed": True,
|
||||
"state": GateState.CLOSED,
|
||||
"epoch": 0,
|
||||
"is_probe": False,
|
||||
"probe_owner": None,
|
||||
"retry_after_s": 0.0,
|
||||
}
|
||||
base.update(overrides)
|
||||
return GateDecision(**base)
|
||||
|
||||
@@ -141,9 +187,15 @@ class TestGateDecisionInvariants:
|
||||
|
||||
class TestGateUpdate:
|
||||
def test_bounds(self):
|
||||
u = GateUpdate(applied=True, state=GateState.CLOSED, epoch=0, failure_count=0, retry_after_s=0.0)
|
||||
u = GateUpdate(
|
||||
applied=True, state=GateState.CLOSED, epoch=0, failure_count=0, retry_after_s=0.0
|
||||
)
|
||||
assert u.applied
|
||||
with pytest.raises(ValueError):
|
||||
GateUpdate(applied=True, state=GateState.CLOSED, epoch=-1, failure_count=0, retry_after_s=0.0)
|
||||
GateUpdate(
|
||||
applied=True, state=GateState.CLOSED, epoch=-1, failure_count=0, retry_after_s=0.0
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
GateUpdate(applied=True, state=GateState.CLOSED, epoch=0, failure_count=-1, retry_after_s=0.0)
|
||||
GateUpdate(
|
||||
applied=True, state=GateState.CLOSED, epoch=0, failure_count=-1, retry_after_s=0.0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user