@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/coordination/reactor.d.ts.map +1 -1
- package/dist/coordination/reactor.js +24 -0
- package/dist/coordination/reactor.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +10 -4
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +316 -193
- package/tools/agents/tests/test_coordination_ban.py +68 -0
- package/tools/agents/tests/test_stream_heartbeat.py +111 -0
|
@@ -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()
|