@miller-tech/uap 1.76.5 → 1.77.0

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.
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ """Streaming keep-alive heartbeat for the guarded-non-stream path.
3
+
4
+ The guarded-non-stream path buffers the ENTIRE upstream generation before
5
+ emitting any SSE bytes, so a long generation sends the client nothing for the
6
+ whole wait and the client's streaming idle-timeout fires -> "API Error".
7
+
8
+ `_heartbeat_then_buffered` wraps the buffered produce coroutine: it emits an
9
+ immediate `message_start`, then `ping` events every PROXY_STREAM_HEARTBEAT_SECS
10
+ while the produce runs, then streams the buffered content (without a second
11
+ message_start). On an error it re-emits the guarded path's error Response as an
12
+ SSE `error` event (the stream has already committed to HTTP 200).
13
+ """
14
+
15
+ import asyncio
16
+ import importlib.util
17
+ import unittest
18
+ from pathlib import Path
19
+
20
+
21
+ def _load():
22
+ p = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
23
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", p)
24
+ m = importlib.util.module_from_spec(spec)
25
+ spec.loader.exec_module(m)
26
+ return m
27
+
28
+
29
+ proxy = _load()
30
+
31
+
32
+ async def _collect(produce_coro, model="test-model"):
33
+ return [chunk async for chunk in proxy._heartbeat_then_buffered(produce_coro, model)]
34
+
35
+
36
+ class TestStreamHeartbeat(unittest.TestCase):
37
+ def setUp(self):
38
+ # small interval so the slow-produce test emits pings quickly
39
+ self._orig = proxy.PROXY_STREAM_HEARTBEAT_SECS
40
+ proxy.PROXY_STREAM_HEARTBEAT_SECS = 0.05
41
+
42
+ def tearDown(self):
43
+ proxy.PROXY_STREAM_HEARTBEAT_SECS = self._orig
44
+
45
+ def test_fast_produce_no_pings_single_message_start(self):
46
+ async def produce():
47
+ return {
48
+ "id": "msg_x",
49
+ "model": "m",
50
+ "content": [{"type": "text", "text": "hi"}],
51
+ "stop_reason": "end_turn",
52
+ "usage": {"output_tokens": 1},
53
+ }
54
+
55
+ out = "".join(asyncio.run(_collect(produce())))
56
+ # exactly one message_start (heartbeat's own; converter skips its own)
57
+ self.assertEqual(out.count("event: message_start"), 1)
58
+ self.assertNotIn("event: ping", out)
59
+ self.assertIn("hi", out)
60
+ self.assertIn("event: message_stop", out)
61
+ self.assertNotIn("event: error", out)
62
+
63
+ def test_slow_produce_emits_pings_then_content(self):
64
+ async def produce():
65
+ await asyncio.sleep(0.18) # > 3 intervals
66
+ return {
67
+ "id": "msg_y",
68
+ "model": "m",
69
+ "content": [{"type": "text", "text": "done"}],
70
+ "stop_reason": "end_turn",
71
+ "usage": {"output_tokens": 1},
72
+ }
73
+
74
+ chunks = asyncio.run(_collect(produce()))
75
+ out = "".join(chunks)
76
+ self.assertEqual(out.count("event: message_start"), 1)
77
+ self.assertGreaterEqual(out.count("event: ping"), 1)
78
+ self.assertIn("done", out)
79
+ # message_start precedes the first ping, ping precedes content
80
+ self.assertLess(out.index("event: message_start"), out.index("event: ping"))
81
+ self.assertLess(out.index("event: ping"), out.index("done"))
82
+
83
+ def test_error_response_becomes_sse_error_event(self):
84
+ import json
85
+
86
+ async def produce():
87
+ return proxy.Response(
88
+ content=json.dumps(
89
+ {"type": "error", "error": {"type": "overloaded_error", "message": "boom"}}
90
+ ),
91
+ status_code=529,
92
+ media_type="application/json",
93
+ )
94
+
95
+ out = "".join(asyncio.run(_collect(produce())))
96
+ self.assertEqual(out.count("event: message_start"), 1)
97
+ self.assertIn("event: error", out)
98
+ self.assertIn("boom", out)
99
+ self.assertNotIn("event: message_stop", out)
100
+
101
+ def test_produce_raises_becomes_sse_error_event(self):
102
+ async def produce():
103
+ raise RuntimeError("kaboom")
104
+
105
+ out = "".join(asyncio.run(_collect(produce())))
106
+ self.assertIn("event: error", out)
107
+ self.assertIn("kaboom", out)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()