@miller-tech/uap 1.179.4 → 1.179.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/guides/PROXY.md +2 -0
- package/package.json +1 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +94 -34
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +225 -0
package/docs/guides/PROXY.md
CHANGED
|
@@ -179,6 +179,8 @@ set is below.
|
|
|
179
179
|
| Env (default) | Behavior |
|
|
180
180
|
|---|---|
|
|
181
181
|
| `PROXY_MAX_CONNECTIONS` (20) | httpx pool size; a large pool + 529 backoff absorbs connection churn gracefully |
|
|
182
|
+
| `PROXY_UPSTREAM_RETRY_MAX` (3) / `PROXY_UPSTREAM_RETRY_DELAY_SECS` (5) | Transient-failure retry budget for upstream calls, on both the buffered and the streaming path. Values below 1 are clamped to 1 |
|
|
183
|
+
| **503 "Loading model"** (no toggle) | llama-server answers 503 while a GGUF is still being mapped. Both paths treat it as transient: wait for `/health` (up to 60s), then retry. A wait that times out surfaces the 503 rather than stacking another — so a cold start costs at most ~60s of extra latency per attempt, not `RETRY_MAX × 60s` of dead air |
|
|
182
184
|
| **529 backpressure** (no toggle) | A pool timeout returns HTTP **529 `overloaded_error`** with `retry-after` — pure graceful degradation, not a hard failure |
|
|
183
185
|
| `PROXY_CLOSEWAIT_REAP_INTERVAL` (0 = **off**) | Opt-in CLOSE-WAIT reaper (pool self-heal); off by default because pool-swap churn can harm a saturated upstream |
|
|
184
186
|
| `PROXY_TOOL_NARROWING` (**off**) | Opt-in: drop cycling/banned tools from the set on loops — always keeps the Bash/WebFetch/Agent escape hatch + write tools (a floor invariant that never strands the agent) |
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -3560,6 +3560,92 @@ class DisconnectAwareClient(httpx.AsyncClient):
|
|
|
3560
3560
|
task.cancel()
|
|
3561
3561
|
|
|
3562
3562
|
|
|
3563
|
+
async def _send_stream_with_retry(
|
|
3564
|
+
client: httpx.AsyncClient,
|
|
3565
|
+
url: str,
|
|
3566
|
+
payload: dict,
|
|
3567
|
+
) -> httpx.Response:
|
|
3568
|
+
"""POST a streaming completion, retrying transient failures and 503 "Loading model".
|
|
3569
|
+
|
|
3570
|
+
The streaming sibling of `_post_with_retry_inner`. Both retry the
|
|
3571
|
+
`_UPSTREAM_RETRY_EXCEPTIONS` set — which is wider than connect errors; a
|
|
3572
|
+
ReadTimeout/ReadError mid-setup is retried too — so a llama-server restart
|
|
3573
|
+
doesn't fail the turn. Both also treat a 503 "Loading model" as transient:
|
|
3574
|
+
llama-server answers that while a GGUF is still being mapped, so the right
|
|
3575
|
+
move is to wait for /health and retry rather than surface a fatal error.
|
|
3576
|
+
|
|
3577
|
+
Raises the last transient exception when every attempt failed. Otherwise
|
|
3578
|
+
returns the response, which may itself be a non-200 the caller must handle —
|
|
3579
|
+
including a loading 503 that outlived the retry budget, or one whose health
|
|
3580
|
+
wait timed out.
|
|
3581
|
+
"""
|
|
3582
|
+
# Clamp: a configured 0 would skip the loop entirely and fall through to the
|
|
3583
|
+
# terminal raise with last_exc unset, escaping the caller's narrow except as
|
|
3584
|
+
# an unhandled 500. One attempt is the floor.
|
|
3585
|
+
retry_max = max(1, PROXY_UPSTREAM_RETRY_MAX)
|
|
3586
|
+
last_exc: Exception | None = None
|
|
3587
|
+
for attempt in range(retry_max):
|
|
3588
|
+
try:
|
|
3589
|
+
resp = await client.send(
|
|
3590
|
+
client.build_request(
|
|
3591
|
+
"POST",
|
|
3592
|
+
url,
|
|
3593
|
+
json=payload,
|
|
3594
|
+
headers={"Content-Type": "application/json"},
|
|
3595
|
+
),
|
|
3596
|
+
stream=True,
|
|
3597
|
+
)
|
|
3598
|
+
setattr(resp, "_uap_client", client)
|
|
3599
|
+
if resp.status_code == 503:
|
|
3600
|
+
# _is_loading_model_503 reads .text, which raises ResponseNotRead
|
|
3601
|
+
# on an unread streaming body — and its bare `except` turns that
|
|
3602
|
+
# into a silent False, so without this read every loading 503
|
|
3603
|
+
# would look permanent and the retry would never fire. httpx
|
|
3604
|
+
# caches the body, so the caller's non-200 handler re-reads free.
|
|
3605
|
+
try:
|
|
3606
|
+
await resp.aread()
|
|
3607
|
+
except BaseException:
|
|
3608
|
+
# Never leave the connection checked out: a ReadError here is
|
|
3609
|
+
# retryable and would otherwise loop with resp unreferenced
|
|
3610
|
+
# and unclosed, accruing CLOSE-WAIT until the pool saturates.
|
|
3611
|
+
_detach_aclose(resp)
|
|
3612
|
+
raise
|
|
3613
|
+
if _is_loading_model_503(resp) and attempt < retry_max - 1:
|
|
3614
|
+
logger.warning(
|
|
3615
|
+
"Upstream 503 Loading model (stream attempt %d/%d) – waiting for health",
|
|
3616
|
+
attempt + 1,
|
|
3617
|
+
retry_max,
|
|
3618
|
+
)
|
|
3619
|
+
healthy = await _wait_for_upstream_health(client, max_wait=60.0)
|
|
3620
|
+
if not healthy:
|
|
3621
|
+
# Matches _post_with_retry_inner: once a health wait has
|
|
3622
|
+
# timed out, stacking another one just buys the client
|
|
3623
|
+
# minutes of dead air before the same failure. Surface it.
|
|
3624
|
+
return resp
|
|
3625
|
+
_detach_aclose(resp)
|
|
3626
|
+
continue
|
|
3627
|
+
return resp
|
|
3628
|
+
except _UPSTREAM_RETRY_EXCEPTIONS as exc:
|
|
3629
|
+
last_exc = exc
|
|
3630
|
+
if attempt < retry_max - 1:
|
|
3631
|
+
logger.warning(
|
|
3632
|
+
"Upstream transient error (stream attempt %d/%d): %s – retrying in %.0fs",
|
|
3633
|
+
attempt + 1,
|
|
3634
|
+
retry_max,
|
|
3635
|
+
type(exc).__name__,
|
|
3636
|
+
PROXY_UPSTREAM_RETRY_DELAY_SECS,
|
|
3637
|
+
)
|
|
3638
|
+
await asyncio.sleep(PROXY_UPSTREAM_RETRY_DELAY_SECS)
|
|
3639
|
+
else:
|
|
3640
|
+
logger.error(
|
|
3641
|
+
"Upstream stream failed after %d attempts: %s: %s",
|
|
3642
|
+
retry_max,
|
|
3643
|
+
type(exc).__name__,
|
|
3644
|
+
exc,
|
|
3645
|
+
)
|
|
3646
|
+
raise last_exc if last_exc else RuntimeError("upstream stream retry failed")
|
|
3647
|
+
|
|
3648
|
+
|
|
3563
3649
|
async def _post_with_retry_inner(
|
|
3564
3650
|
client: httpx.AsyncClient,
|
|
3565
3651
|
url: str,
|
|
@@ -11676,43 +11762,17 @@ async def messages(request: Request):
|
|
|
11676
11762
|
# Retry upstream connection with backoff to handle
|
|
11677
11763
|
# llama-server restarts gracefully instead of 500-ing to the client.
|
|
11678
11764
|
MAX_UPSTREAM_RETRIES = PROXY_UPSTREAM_RETRY_MAX
|
|
11679
|
-
RETRY_DELAY_SECS = PROXY_UPSTREAM_RETRY_DELAY_SECS
|
|
11680
11765
|
last_exc: Exception | None = None
|
|
11681
11766
|
resp: httpx.Response | None = None
|
|
11682
11767
|
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
),
|
|
11692
|
-
stream=True,
|
|
11693
|
-
)
|
|
11694
|
-
setattr(resp, "_uap_client", client)
|
|
11695
|
-
# Connection succeeded – break out of retry loop
|
|
11696
|
-
last_exc = None
|
|
11697
|
-
break
|
|
11698
|
-
except _UPSTREAM_RETRY_EXCEPTIONS as exc:
|
|
11699
|
-
last_exc = exc
|
|
11700
|
-
if attempt < MAX_UPSTREAM_RETRIES - 1:
|
|
11701
|
-
logger.warning(
|
|
11702
|
-
"Upstream connect failed (attempt %d/%d): %s – retrying in %.0fs",
|
|
11703
|
-
attempt + 1,
|
|
11704
|
-
MAX_UPSTREAM_RETRIES,
|
|
11705
|
-
type(exc).__name__,
|
|
11706
|
-
RETRY_DELAY_SECS,
|
|
11707
|
-
)
|
|
11708
|
-
await asyncio.sleep(RETRY_DELAY_SECS)
|
|
11709
|
-
else:
|
|
11710
|
-
logger.error(
|
|
11711
|
-
"Upstream connect failed after %d attempts: %s: %s",
|
|
11712
|
-
MAX_UPSTREAM_RETRIES,
|
|
11713
|
-
type(exc).__name__,
|
|
11714
|
-
exc,
|
|
11715
|
-
)
|
|
11768
|
+
try:
|
|
11769
|
+
resp = await _send_stream_with_retry(
|
|
11770
|
+
client,
|
|
11771
|
+
f"{LLAMA_CPP_BASE}/chat/completions",
|
|
11772
|
+
openai_body,
|
|
11773
|
+
)
|
|
11774
|
+
except _UPSTREAM_RETRY_EXCEPTIONS as exc:
|
|
11775
|
+
last_exc = exc
|
|
11716
11776
|
|
|
11717
11777
|
if last_exc is not None:
|
|
11718
11778
|
return Response(
|
|
@@ -5834,3 +5834,228 @@ class TestPassthroughTimeout(unittest.IsolatedAsyncioTestCase):
|
|
|
5834
5834
|
|
|
5835
5835
|
self.assertIsNotNone(captured.get("timeout"))
|
|
5836
5836
|
self.assertEqual(captured["timeout"].read, proxy.PROXY_PASSTHROUGH_TIMEOUT)
|
|
5837
|
+
|
|
5838
|
+
|
|
5839
|
+
class _FakeStreamResponse:
|
|
5840
|
+
"""Stand-in for a streamed httpx.Response, faithful to two contracts.
|
|
5841
|
+
|
|
5842
|
+
Both are load-bearing for _send_stream_with_retry, and a fake that models
|
|
5843
|
+
neither lets the fix be deleted with the suite still green:
|
|
5844
|
+
|
|
5845
|
+
1. `.text` raises ResponseNotRead until the body has been read. That is why
|
|
5846
|
+
the production code calls aread() before _is_loading_model_503 — whose
|
|
5847
|
+
bare `except` would otherwise swallow the raise into a silent False and
|
|
5848
|
+
silently disable the retry.
|
|
5849
|
+
2. aread() caches. The caller re-reads the same response in its non-200
|
|
5850
|
+
handler, which only works because httpx serves the second read from
|
|
5851
|
+
cache rather than raising StreamConsumed.
|
|
5852
|
+
"""
|
|
5853
|
+
|
|
5854
|
+
def __init__(self, status_code, text=""):
|
|
5855
|
+
self.status_code = status_code
|
|
5856
|
+
self._body = text
|
|
5857
|
+
self._read = False
|
|
5858
|
+
self.read_count = 0
|
|
5859
|
+
self.closed = False
|
|
5860
|
+
|
|
5861
|
+
@property
|
|
5862
|
+
def text(self):
|
|
5863
|
+
if not self._read:
|
|
5864
|
+
raise httpx.ResponseNotRead()
|
|
5865
|
+
return self._body
|
|
5866
|
+
|
|
5867
|
+
async def aread(self):
|
|
5868
|
+
if self._read:
|
|
5869
|
+
return self._body.encode() # cached: no second stream consumption
|
|
5870
|
+
self._read = True
|
|
5871
|
+
self.read_count += 1
|
|
5872
|
+
return self._body.encode()
|
|
5873
|
+
|
|
5874
|
+
async def aclose(self):
|
|
5875
|
+
self.closed = True
|
|
5876
|
+
|
|
5877
|
+
|
|
5878
|
+
class _FakeStreamClient:
|
|
5879
|
+
"""Replays a queued script of responses/exceptions for client.send()."""
|
|
5880
|
+
|
|
5881
|
+
def __init__(self, scripted):
|
|
5882
|
+
self._scripted = list(scripted)
|
|
5883
|
+
self.sends = 0
|
|
5884
|
+
self.requests = []
|
|
5885
|
+
self.stream_flags = []
|
|
5886
|
+
|
|
5887
|
+
def build_request(self, method, url, **kwargs):
|
|
5888
|
+
return {"method": method, "url": url, **kwargs}
|
|
5889
|
+
|
|
5890
|
+
async def send(self, request, stream=False):
|
|
5891
|
+
self.sends += 1
|
|
5892
|
+
self.requests.append(request)
|
|
5893
|
+
self.stream_flags.append(stream)
|
|
5894
|
+
if not self._scripted:
|
|
5895
|
+
raise AssertionError("no scripted response left")
|
|
5896
|
+
nxt = self._scripted.pop(0)
|
|
5897
|
+
if isinstance(nxt, Exception):
|
|
5898
|
+
raise nxt
|
|
5899
|
+
return nxt
|
|
5900
|
+
|
|
5901
|
+
|
|
5902
|
+
class TestSendStreamWithRetry(unittest.TestCase):
|
|
5903
|
+
"""The streaming path must treat 503 'Loading model' as transient.
|
|
5904
|
+
|
|
5905
|
+
Regression guard: this path used to break out of its retry loop on any HTTP
|
|
5906
|
+
response, so a streamed turn that began while llama-server was still mapping
|
|
5907
|
+
a GGUF surfaced 'AI_APICallError: Loading model' to the client as a fatal
|
|
5908
|
+
error. The non-streaming sibling (_post_with_retry_inner) retried it —
|
|
5909
|
+
health-gated and bounded by PROXY_UPSTREAM_RETRY_MAX — while the streaming
|
|
5910
|
+
path, the one every opencode turn actually uses, did not retry it at all.
|
|
5911
|
+
"""
|
|
5912
|
+
|
|
5913
|
+
def _run(self, client, health_ok=True, retry_max=3):
|
|
5914
|
+
waited = []
|
|
5915
|
+
|
|
5916
|
+
async def _fake_health(_client, max_wait=60.0, poll_interval=5.0):
|
|
5917
|
+
waited.append(max_wait)
|
|
5918
|
+
return health_ok
|
|
5919
|
+
|
|
5920
|
+
async def _no_sleep(_secs):
|
|
5921
|
+
return None
|
|
5922
|
+
|
|
5923
|
+
def _sync_close(closeable):
|
|
5924
|
+
# Production schedules the close on a detached task; inside a test
|
|
5925
|
+
# the loop closes before it runs. Close eagerly so `.closed` is a
|
|
5926
|
+
# deterministic assertion rather than a race.
|
|
5927
|
+
closeable.closed = True
|
|
5928
|
+
|
|
5929
|
+
with unittest.mock.patch.object(proxy, "_wait_for_upstream_health", _fake_health), \
|
|
5930
|
+
unittest.mock.patch.object(proxy, "_detach_aclose", _sync_close), \
|
|
5931
|
+
unittest.mock.patch.object(proxy, "PROXY_UPSTREAM_RETRY_MAX", retry_max), \
|
|
5932
|
+
unittest.mock.patch.object(asyncio, "sleep", _no_sleep):
|
|
5933
|
+
resp = asyncio.run(
|
|
5934
|
+
proxy._send_stream_with_retry(client, "http://x/v1/chat/completions", {})
|
|
5935
|
+
)
|
|
5936
|
+
return resp, waited
|
|
5937
|
+
|
|
5938
|
+
def test_retries_loading_model_503_then_returns_success(self):
|
|
5939
|
+
loading = _FakeStreamResponse(503, "Loading model")
|
|
5940
|
+
ok = _FakeStreamResponse(200, "")
|
|
5941
|
+
client = _FakeStreamClient([loading, ok])
|
|
5942
|
+
|
|
5943
|
+
resp, waited = self._run(client)
|
|
5944
|
+
|
|
5945
|
+
self.assertIs(resp, ok)
|
|
5946
|
+
self.assertEqual(client.sends, 2, "should have retried after the 503")
|
|
5947
|
+
self.assertEqual(len(waited), 1, "should have waited for upstream health")
|
|
5948
|
+
self.assertTrue(loading.closed, "the discarded 503 must be closed")
|
|
5949
|
+
|
|
5950
|
+
def test_does_not_retry_a_non_loading_503(self):
|
|
5951
|
+
# A 503 that is not a model load is a real upstream failure; passing it
|
|
5952
|
+
# straight through preserves the existing error contract.
|
|
5953
|
+
overloaded = _FakeStreamResponse(503, "server is overloaded")
|
|
5954
|
+
client = _FakeStreamClient([overloaded, _FakeStreamResponse(200, "")])
|
|
5955
|
+
|
|
5956
|
+
resp, waited = self._run(client)
|
|
5957
|
+
|
|
5958
|
+
self.assertIs(resp, overloaded)
|
|
5959
|
+
self.assertEqual(client.sends, 1)
|
|
5960
|
+
self.assertEqual(waited, [])
|
|
5961
|
+
|
|
5962
|
+
def test_returns_503_after_exhausting_retry_budget(self):
|
|
5963
|
+
responses = [_FakeStreamResponse(503, "Loading model") for _ in range(3)]
|
|
5964
|
+
client = _FakeStreamClient(list(responses))
|
|
5965
|
+
|
|
5966
|
+
resp, waited = self._run(client, retry_max=3)
|
|
5967
|
+
|
|
5968
|
+
self.assertEqual(client.sends, 3)
|
|
5969
|
+
self.assertEqual(resp.status_code, 503)
|
|
5970
|
+
self.assertIs(resp, responses[-1], "last 503 is surfaced to the caller")
|
|
5971
|
+
self.assertEqual(len(waited), 2, "waits between attempts, not after the last")
|
|
5972
|
+
|
|
5973
|
+
def test_surfaces_the_503_when_the_health_wait_times_out(self):
|
|
5974
|
+
# Mirrors _post_with_retry_inner: once upstream is still unhealthy after
|
|
5975
|
+
# a full wait, stacking more waits only buys the client dead air. Without
|
|
5976
|
+
# this gate a dead upstream costs retry_max x 60s before the same failure.
|
|
5977
|
+
loading = _FakeStreamResponse(503, "Loading model")
|
|
5978
|
+
client = _FakeStreamClient([loading, _FakeStreamResponse(200, "")])
|
|
5979
|
+
|
|
5980
|
+
resp, waited = self._run(client, health_ok=False)
|
|
5981
|
+
|
|
5982
|
+
self.assertIs(resp, loading, "unhealthy upstream surfaces the 503 now")
|
|
5983
|
+
self.assertEqual(client.sends, 1, "must not retry a still-unhealthy upstream")
|
|
5984
|
+
self.assertEqual(len(waited), 1)
|
|
5985
|
+
|
|
5986
|
+
def test_reads_the_body_before_classifying_a_503(self):
|
|
5987
|
+
# The fix hinges on aread() preceding _is_loading_model_503, whose bare
|
|
5988
|
+
# `except` would turn httpx's ResponseNotRead into a silent False and
|
|
5989
|
+
# disable the retry entirely. Deleting that aread() must fail here.
|
|
5990
|
+
loading = _FakeStreamResponse(503, "Loading model")
|
|
5991
|
+
ok = _FakeStreamResponse(200, "")
|
|
5992
|
+
client = _FakeStreamClient([loading, ok])
|
|
5993
|
+
|
|
5994
|
+
resp, _ = self._run(client)
|
|
5995
|
+
|
|
5996
|
+
self.assertIs(resp, ok)
|
|
5997
|
+
self.assertEqual(loading.read_count, 1, "body read exactly once, before .text")
|
|
5998
|
+
|
|
5999
|
+
def test_closes_the_response_when_the_body_read_fails(self):
|
|
6000
|
+
# A ReadError while reading the 503 body is retryable; if the discarded
|
|
6001
|
+
# response is not closed the connection accrues as CLOSE-WAIT until the
|
|
6002
|
+
# pool saturates, with the client seeing nothing wrong.
|
|
6003
|
+
class _ExplodingResponse(_FakeStreamResponse):
|
|
6004
|
+
async def aread(self):
|
|
6005
|
+
raise httpx.ReadError("reset mid-body")
|
|
6006
|
+
|
|
6007
|
+
boom = _ExplodingResponse(503, "Loading model")
|
|
6008
|
+
ok = _FakeStreamResponse(200, "")
|
|
6009
|
+
client = _FakeStreamClient([boom, ok])
|
|
6010
|
+
|
|
6011
|
+
resp, _ = self._run(client)
|
|
6012
|
+
|
|
6013
|
+
self.assertIs(resp, ok, "the read failure is retried")
|
|
6014
|
+
self.assertTrue(boom.closed, "the abandoned response must be closed")
|
|
6015
|
+
|
|
6016
|
+
def test_still_retries_connect_errors(self):
|
|
6017
|
+
# Regression guard for the pre-existing behaviour the extraction replaced.
|
|
6018
|
+
ok = _FakeStreamResponse(200, "")
|
|
6019
|
+
client = _FakeStreamClient([httpx.ConnectError("boom"), ok])
|
|
6020
|
+
|
|
6021
|
+
resp, _ = self._run(client)
|
|
6022
|
+
|
|
6023
|
+
self.assertIs(resp, ok)
|
|
6024
|
+
self.assertEqual(client.sends, 2)
|
|
6025
|
+
|
|
6026
|
+
def test_raises_when_every_connect_attempt_fails(self):
|
|
6027
|
+
client = _FakeStreamClient([httpx.ConnectError("boom") for _ in range(3)])
|
|
6028
|
+
|
|
6029
|
+
with self.assertRaises(httpx.ConnectError):
|
|
6030
|
+
self._run(client, retry_max=3)
|
|
6031
|
+
|
|
6032
|
+
self.assertEqual(client.sends, 3)
|
|
6033
|
+
|
|
6034
|
+
def test_retry_max_of_zero_still_makes_one_attempt(self):
|
|
6035
|
+
# An unclamped 0 skipped the loop and raised a bare RuntimeError, which
|
|
6036
|
+
# the call site's narrow `except` misses — an unhandled 500 where the
|
|
6037
|
+
# old code returned a clean 529.
|
|
6038
|
+
ok = _FakeStreamResponse(200, "")
|
|
6039
|
+
client = _FakeStreamClient([ok])
|
|
6040
|
+
|
|
6041
|
+
resp, _ = self._run(client, retry_max=0)
|
|
6042
|
+
|
|
6043
|
+
self.assertIs(resp, ok)
|
|
6044
|
+
self.assertEqual(client.sends, 1)
|
|
6045
|
+
|
|
6046
|
+
def test_preserves_the_streaming_contract_the_caller_depends_on(self):
|
|
6047
|
+
# stream=True keeps the turn incremental (buffering it would silently
|
|
6048
|
+
# reintroduce full-generation latency), and _uap_client drives the
|
|
6049
|
+
# inflight accounting that the pool-retire logic waits on.
|
|
6050
|
+
ok = _FakeStreamResponse(200, "")
|
|
6051
|
+
client = _FakeStreamClient([ok])
|
|
6052
|
+
|
|
6053
|
+
resp, _ = self._run(client)
|
|
6054
|
+
|
|
6055
|
+
self.assertEqual(client.stream_flags, [True])
|
|
6056
|
+
self.assertIs(getattr(resp, "_uap_client"), client)
|
|
6057
|
+
self.assertEqual(client.requests[0]["url"], "http://x/v1/chat/completions")
|
|
6058
|
+
|
|
6059
|
+
|
|
6060
|
+
if __name__ == "__main__":
|
|
6061
|
+
unittest.main()
|