@miller-tech/uap 1.129.1 → 1.129.2
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/bin/cli.js +6 -1
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/dashboard.d.ts +2 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +5 -0
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +11 -2
- package/dist/dashboard/server.js.map +1 -1
- package/docs/reference/CLI.md +1 -1
- package/docs/reference/CONFIGURATION.md +1 -0
- package/package.json +2 -2
- package/src/policies/enforcers/__pycache__/_common.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 +31 -0
- package/tools/agents/tests/test_stream_telemetry.py +82 -0
- package/web/dashboard.html +4 -1
|
@@ -8954,6 +8954,12 @@ async def stream_anthropic_response(
|
|
|
8954
8954
|
text_chunks: list[str] = [] # accumulate text for logging
|
|
8955
8955
|
reasoning_chunks: list[str] = [] # accumulate reasoning for fallback
|
|
8956
8956
|
|
|
8957
|
+
# Real token counts from upstream's final usage chunk (llama-server /
|
|
8958
|
+
# OpenAI emit it on the last data frame). The per-delta output_tokens
|
|
8959
|
+
# counter is a chunk count that misses tool-call deltas entirely; prefer
|
|
8960
|
+
# upstream truth for telemetry and the session monitor.
|
|
8961
|
+
upstream_usage: dict = {}
|
|
8962
|
+
|
|
8957
8963
|
try:
|
|
8958
8964
|
async for line in openai_stream.aiter_lines():
|
|
8959
8965
|
if not line.startswith("data: "):
|
|
@@ -8966,6 +8972,9 @@ async def stream_anthropic_response(
|
|
|
8966
8972
|
except json.JSONDecodeError:
|
|
8967
8973
|
continue
|
|
8968
8974
|
|
|
8975
|
+
if isinstance(chunk.get("usage"), dict):
|
|
8976
|
+
upstream_usage = chunk["usage"]
|
|
8977
|
+
|
|
8969
8978
|
choice = (chunk.get("choices") or [{}])[0]
|
|
8970
8979
|
delta = choice.get("delta", {})
|
|
8971
8980
|
|
|
@@ -9239,6 +9248,28 @@ async def stream_anthropic_response(
|
|
|
9239
9248
|
# message_stop
|
|
9240
9249
|
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
9241
9250
|
|
|
9251
|
+
# Per-project telemetry for the TRUE streaming path. The only other call
|
|
9252
|
+
# sites are on the non-stream / guarded-non-stream paths, so streaming
|
|
9253
|
+
# sessions (the client default) wrote no task_outcomes rows and
|
|
9254
|
+
# per-project dashboards froze at the last non-stream response.
|
|
9255
|
+
# Prefer upstream's real usage: last_input_tokens is a char-based request
|
|
9256
|
+
# estimate, and the per-delta counter misses tool-call turns entirely.
|
|
9257
|
+
upstream_input = int(upstream_usage.get("prompt_tokens") or 0)
|
|
9258
|
+
if upstream_input > 0:
|
|
9259
|
+
monitor.last_input_tokens = upstream_input
|
|
9260
|
+
usage_final = {
|
|
9261
|
+
"input_tokens": upstream_input or getattr(monitor, "last_input_tokens", 0) or 0,
|
|
9262
|
+
"output_tokens": int(upstream_usage.get("completion_tokens") or 0) or output_tokens,
|
|
9263
|
+
}
|
|
9264
|
+
# After message_stop (never delays the client's final frame; a disconnect
|
|
9265
|
+
# parked exactly on that yield skips recording, matching the non-stream
|
|
9266
|
+
# "completed responses only" semantics) and off the event loop (a locked
|
|
9267
|
+
# analytics DB must not stall token delivery for concurrent sessions).
|
|
9268
|
+
try:
|
|
9269
|
+
await asyncio.to_thread(_record_project_telemetry, anthropic_body, model, usage_final)
|
|
9270
|
+
except Exception:
|
|
9271
|
+
pass
|
|
9272
|
+
|
|
9242
9273
|
|
|
9243
9274
|
# ===========================================================================
|
|
9244
9275
|
# API Endpoints
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The TRUE streaming path must record per-project telemetry.
|
|
2
|
+
|
|
3
|
+
Regression for the frozen-dashboard incident (2026-07-08): the only
|
|
4
|
+
_record_project_telemetry call sites were on the non-stream / guarded
|
|
5
|
+
non-stream paths, so streaming sessions (the client default) wrote no
|
|
6
|
+
task_outcomes rows and per-project dashboards showed static data forever.
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import importlib.util
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
14
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
15
|
+
ap = importlib.util.module_from_spec(spec)
|
|
16
|
+
spec.loader.exec_module(ap)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeOpenAIStream:
|
|
20
|
+
"""Minimal httpx.Response stand-in: yields OpenAI SSE lines."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, lines):
|
|
23
|
+
self._lines = lines
|
|
24
|
+
|
|
25
|
+
async def aiter_lines(self):
|
|
26
|
+
for line in self._lines:
|
|
27
|
+
yield line
|
|
28
|
+
|
|
29
|
+
async def aclose(self):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
OPENAI_LINES = [
|
|
34
|
+
'data: {"choices": [{"delta": {"content": "hello"}, "finish_reason": null}]}',
|
|
35
|
+
'data: {"choices": [{"delta": {}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 456, "completion_tokens": 5}}',
|
|
36
|
+
"data: [DONE]",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StreamTelemetryTest(unittest.TestCase):
|
|
41
|
+
def _drive(self, body):
|
|
42
|
+
monitor = ap.SessionMonitor()
|
|
43
|
+
monitor.last_input_tokens = 123
|
|
44
|
+
recorded = []
|
|
45
|
+
saved = ap._record_project_telemetry
|
|
46
|
+
ap._record_project_telemetry = lambda b, m, u: recorded.append((b, m, u))
|
|
47
|
+
try:
|
|
48
|
+
gen = ap.stream_anthropic_response(
|
|
49
|
+
FakeOpenAIStream(OPENAI_LINES), "test-model", monitor, body
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def consume():
|
|
53
|
+
return [frame async for frame in gen]
|
|
54
|
+
|
|
55
|
+
frames = asyncio.run(consume())
|
|
56
|
+
finally:
|
|
57
|
+
ap._record_project_telemetry = saved
|
|
58
|
+
return frames, recorded
|
|
59
|
+
|
|
60
|
+
def test_streaming_response_records_project_telemetry(self):
|
|
61
|
+
body = {"messages": [{"role": "user", "content": "hi"}]}
|
|
62
|
+
frames, recorded = self._drive(body)
|
|
63
|
+
self.assertTrue(any("message_stop" in f for f in frames))
|
|
64
|
+
self.assertEqual(len(recorded), 1)
|
|
65
|
+
recorded_body, model, usage = recorded[0]
|
|
66
|
+
self.assertIs(recorded_body, body)
|
|
67
|
+
self.assertEqual(model, "test-model")
|
|
68
|
+
# Upstream's final usage chunk wins over the request-side estimate and
|
|
69
|
+
# the per-delta chunk counter (which misses tool-call turns).
|
|
70
|
+
self.assertEqual(usage.get("input_tokens"), 456)
|
|
71
|
+
self.assertEqual(usage.get("output_tokens"), 5)
|
|
72
|
+
|
|
73
|
+
def test_telemetry_recorded_after_client_visible_stream_end(self):
|
|
74
|
+
# The recorder must not delay or reorder client frames: message_stop is
|
|
75
|
+
# emitted before the telemetry write happens.
|
|
76
|
+
frames, recorded = self._drive({"messages": []})
|
|
77
|
+
self.assertIn("message_stop", frames[-1])
|
|
78
|
+
self.assertEqual(len(recorded), 1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
unittest.main()
|
package/web/dashboard.html
CHANGED
|
@@ -629,6 +629,9 @@
|
|
|
629
629
|
// page is opened without the server substitution (mutations then 401).
|
|
630
630
|
const DASHBOARD_TOKEN = '__UAP_DASHBOARD_TOKEN__'.startsWith('__UAP') ? '' : '__UAP_DASHBOARD_TOKEN__';
|
|
631
631
|
const MUTATION_HEADERS = { 'X-Uap-Dashboard-Token': DASHBOARD_TOKEN };
|
|
632
|
+
// Server-injected push cadence (uap dash serve --refresh); the fallback
|
|
633
|
+
// poll below matches it. 2000 when opened without server substitution.
|
|
634
|
+
const REFRESH_MS = Number('__UAP_DASH_REFRESH_MS__') > 0 ? Number('__UAP_DASH_REFRESH_MS__') : 2000;
|
|
632
635
|
let ws = null;
|
|
633
636
|
let reconnectTimer = null;
|
|
634
637
|
let reconnectDelay = 1000;
|
|
@@ -1281,7 +1284,7 @@
|
|
|
1281
1284
|
setInterval(async()=>{
|
|
1282
1285
|
if(ws&&ws.readyState===WebSocket.OPEN)return;
|
|
1283
1286
|
try{const r=await fetch(API_URL+'/api/dashboard');if(!r.ok)return;const d=await r.json();if(d)render(d);document.getElementById('ws-status').className='status-dot polling';document.getElementById('refresh-info').textContent='Polling - '+new Date().toLocaleTimeString();document.title='UAP Dashboard - Polling';}catch{}
|
|
1284
|
-
},
|
|
1287
|
+
}, REFRESH_MS);
|
|
1285
1288
|
|
|
1286
1289
|
// Resize: re-init charts
|
|
1287
1290
|
let resizeT;
|