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:
@@ -22,10 +22,14 @@ from polygateway.types import SourceConfig
|
||||
|
||||
|
||||
def _source(**overrides):
|
||||
base = dict(
|
||||
name="qwen_1", provider="qwen", base_url="https://gw.example/v1",
|
||||
api_key="sk-test", model="qwen-max", timeout_s=5.0,
|
||||
)
|
||||
base = {
|
||||
"name": "qwen_1",
|
||||
"provider": "qwen",
|
||||
"base_url": "https://gw.example/v1",
|
||||
"api_key": "sk-test",
|
||||
"model": "qwen-max",
|
||||
"timeout_s": 5.0,
|
||||
}
|
||||
base.update(overrides)
|
||||
return SourceConfig(**base)
|
||||
|
||||
@@ -47,9 +51,7 @@ _USAGE = {"prompt_tokens": 11, "completion_tokens": 7}
|
||||
|
||||
def _sse_stream(*frames, done=True):
|
||||
text = "".join(frames) + ("data: [DONE]\n\n" if done else "")
|
||||
return httpx.Response(
|
||||
200, content=text.encode(), headers={"content-type": "text/event-stream"}
|
||||
)
|
||||
return httpx.Response(200, content=text.encode(), headers={"content-type": "text/event-stream"})
|
||||
|
||||
|
||||
def _transport_for(handler):
|
||||
@@ -61,8 +63,11 @@ def _transport_for(handler):
|
||||
|
||||
async def _complete(transport, source, *, stream=True, overlay=None):
|
||||
return await transport.complete(
|
||||
messages=[{"role": "user", "content": "hi"}], source=source,
|
||||
stream=stream, overlay=overlay or {}, call_id="cid-1",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
source=source,
|
||||
stream=stream,
|
||||
overlay=overlay or {},
|
||||
call_id="cid-1",
|
||||
)
|
||||
|
||||
|
||||
@@ -76,8 +81,13 @@ class TestSsePureFunctions:
|
||||
|
||||
async def test_iter_deltas_yields_and_flags_done(self):
|
||||
async def lines():
|
||||
for raw in [_chunk(content="he"), ": ping", _chunk(reasoning="think"),
|
||||
_chunk(usage=_USAGE), "data: [DONE]"]:
|
||||
for raw in [
|
||||
_chunk(content="he"),
|
||||
": ping",
|
||||
_chunk(reasoning="think"),
|
||||
_chunk(usage=_USAGE),
|
||||
"data: [DONE]",
|
||||
]:
|
||||
for line in raw.splitlines():
|
||||
yield line
|
||||
|
||||
@@ -97,8 +107,12 @@ class TestSsePureFunctions:
|
||||
class TestStreamHappyPath:
|
||||
async def test_full_stream_with_usage(self):
|
||||
def handler(request):
|
||||
return _sse_stream(_chunk(reasoning="ponder"), _chunk(content="hello"),
|
||||
_chunk(content=" world"), _chunk(usage=_USAGE))
|
||||
return _sse_stream(
|
||||
_chunk(reasoning="ponder"),
|
||||
_chunk(content="hello"),
|
||||
_chunk(content=" world"),
|
||||
_chunk(usage=_USAGE),
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source())
|
||||
assert result.content == "hello world"
|
||||
@@ -152,10 +166,13 @@ class TestNonStreamFastPath:
|
||||
def handler(request):
|
||||
body = json.loads(request.content)
|
||||
assert body.get("stream") is False and "stream_options" not in body
|
||||
return httpx.Response(200, json={
|
||||
"choices": [{"message": {"content": "42", "reasoning_content": "count"}}],
|
||||
"usage": _USAGE,
|
||||
})
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"choices": [{"message": {"content": "42", "reasoning_content": "count"}}],
|
||||
"usage": _USAGE,
|
||||
},
|
||||
)
|
||||
|
||||
result = await _complete(_transport_for(handler), _source(), stream=False)
|
||||
assert result.content == "42" and result.thinking == "count"
|
||||
@@ -189,7 +206,8 @@ class TestRequestShaping:
|
||||
return _sse_stream(_chunk(content="x"), _chunk(usage=_USAGE))
|
||||
|
||||
await _complete(
|
||||
_transport_for(handler), _source(),
|
||||
_transport_for(handler),
|
||||
_source(),
|
||||
overlay={"response_format": {"type": "json_object"}},
|
||||
)
|
||||
assert seen["response_format"] == {"type": "json_object"}
|
||||
@@ -222,8 +240,9 @@ class TestErrorTranslation:
|
||||
|
||||
async def test_retry_after_http_date_ignored(self):
|
||||
def handler(request):
|
||||
return httpx.Response(429, content=b"{}",
|
||||
headers={"retry-after": "Wed, 21 Oct 2026 07:28:00 GMT"})
|
||||
return httpx.Response(
|
||||
429, content=b"{}", headers={"retry-after": "Wed, 21 Oct 2026 07:28:00 GMT"}
|
||||
)
|
||||
|
||||
with pytest.raises(TransientError) as ei:
|
||||
await _complete(_transport_for(handler), _source())
|
||||
|
||||
Reference in New Issue
Block a user