coze_lab 0.1.50 → 0.1.52
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/index.js
CHANGED
|
@@ -20,12 +20,43 @@ class CloudAbort extends Error {}
|
|
|
20
20
|
// 兼容两种形态:JSON 里的 "logid":"xxx" / detail.logid,以及裸 logid=xxx。
|
|
21
21
|
function extractLogid(text) {
|
|
22
22
|
if (!text) return '';
|
|
23
|
-
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9]+)"/);
|
|
23
|
+
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
24
24
|
if (m) return m[1];
|
|
25
|
-
m = text.match(/
|
|
25
|
+
m = text.match(/"log_id"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
26
|
+
if (m) return m[1];
|
|
27
|
+
m = text.match(/"Logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
28
|
+
if (m) return m[1];
|
|
29
|
+
m = text.match(/logid[=:\s]+([a-zA-Z0-9._:-]+)/i);
|
|
26
30
|
return m ? m[1] : '';
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
function headerString(value) {
|
|
34
|
+
if (Array.isArray(value)) return value.find(Boolean) || '';
|
|
35
|
+
return value ? String(value) : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractLogidFromHeaders(headers) {
|
|
39
|
+
if (!headers) return '';
|
|
40
|
+
const candidates = [
|
|
41
|
+
'x-tt-logid',
|
|
42
|
+
'x-tt-log-id',
|
|
43
|
+
'x-log-id',
|
|
44
|
+
'x-logid',
|
|
45
|
+
'x-request-id',
|
|
46
|
+
'x-tt-trace-id',
|
|
47
|
+
'trace-id',
|
|
48
|
+
];
|
|
49
|
+
for (const key of candidates) {
|
|
50
|
+
const value = headerString(headers[key] || headers[key.toLowerCase()] || headers[key.toUpperCase()]);
|
|
51
|
+
if (value) return value;
|
|
52
|
+
}
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatLogid(logid) {
|
|
57
|
+
return logid ? `, logid=${logid}` : ', logid=';
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
// 输出结构化结果行(仅云端模式)。
|
|
30
61
|
function emitCloudResult() {
|
|
31
62
|
if (!CLOUD_MODE) return;
|
|
@@ -1108,7 +1139,7 @@ function httpsPost(url, body, extraHeaders) {
|
|
|
1108
1139
|
(res) => {
|
|
1109
1140
|
let buf = '';
|
|
1110
1141
|
res.on('data', c => buf += c);
|
|
1111
|
-
res.on('end', () => resolve({ status: res.statusCode, body: buf }));
|
|
1142
|
+
res.on('end', () => resolve({ status: res.statusCode, body: buf, headers: res.headers || {}, logid: extractLogidFromHeaders(res.headers) || extractLogid(buf) }));
|
|
1112
1143
|
}
|
|
1113
1144
|
);
|
|
1114
1145
|
req.on('error', reject);
|
|
@@ -1166,7 +1197,7 @@ def extract_logid(text):
|
|
|
1166
1197
|
if idx >= 0:
|
|
1167
1198
|
out = []
|
|
1168
1199
|
for ch in text[idx + len(marker):]:
|
|
1169
|
-
if ch.isalnum():
|
|
1200
|
+
if ch.isalnum() or ch in "._:-":
|
|
1170
1201
|
out.append(ch)
|
|
1171
1202
|
else:
|
|
1172
1203
|
break
|
|
@@ -1255,7 +1286,7 @@ except Exception as e:
|
|
|
1255
1286
|
const body = parsed?.body || result.stderr || result.stdout || (result.timedOut ? 'SDK selfcheck timed out' : '');
|
|
1256
1287
|
const success = result.code === 0 && parsed?.success === true;
|
|
1257
1288
|
if (success) {
|
|
1258
|
-
ok(`trace 上报成功 (pair_code=${pair})`);
|
|
1289
|
+
ok(`trace 上报成功 (pair_code=${pair}${formatLogid(parsed?.logid || '')})`);
|
|
1259
1290
|
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
1260
1291
|
} else {
|
|
1261
1292
|
warn(`trace 上报失败: SDK selfcheck exit ${result.code}`);
|
|
@@ -1263,7 +1294,7 @@ except Exception as e:
|
|
|
1263
1294
|
const snippet = String(body || '').slice(0, 300);
|
|
1264
1295
|
if (snippet) console.log(snippet);
|
|
1265
1296
|
}
|
|
1266
|
-
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || '' };
|
|
1297
|
+
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || extractLogid(body) || '' };
|
|
1267
1298
|
}
|
|
1268
1299
|
|
|
1269
1300
|
async function verifyCloudTraceReport(token, workspaceId, pairCode, tokenSource) {
|
|
@@ -1330,14 +1361,14 @@ async function verifyCloudTraceReport(token, workspaceId, pairCode, tokenSource)
|
|
|
1330
1361
|
);
|
|
1331
1362
|
const success = res.status >= 200 && res.status < 300;
|
|
1332
1363
|
if (success) {
|
|
1333
|
-
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
|
|
1364
|
+
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair}${formatLogid(res.logid || '')})`);
|
|
1334
1365
|
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
1335
1366
|
} else {
|
|
1336
|
-
warn(`trace 上报失败: HTTP ${res.status}`);
|
|
1367
|
+
warn(`trace 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')}`);
|
|
1337
1368
|
const snippet = (res.body || '').slice(0, 300);
|
|
1338
1369
|
if (snippet) console.log(snippet);
|
|
1339
1370
|
}
|
|
1340
|
-
return { success, status: res.status, body: res.body || '', traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: extractLogid(res.body || '') };
|
|
1371
|
+
return { success, status: res.status, body: res.body || '', traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: res.logid || extractLogid(res.body || '') };
|
|
1341
1372
|
} catch (e) {
|
|
1342
1373
|
warn(`trace 上报失败: ${e.message}`);
|
|
1343
1374
|
return { success: false, status: 0, body: e.message, traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: extractLogid(e.message) };
|
|
@@ -1396,14 +1427,14 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
|
|
|
1396
1427
|
|
|
1397
1428
|
const success = res.status >= 200 && res.status < 300;
|
|
1398
1429
|
if (success) {
|
|
1399
|
-
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
|
|
1430
|
+
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair}${formatLogid(res.logid || '')})`);
|
|
1400
1431
|
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
1401
1432
|
} else {
|
|
1402
|
-
warn(`trace 上报失败: HTTP ${res.status}`);
|
|
1433
|
+
warn(`trace 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')}`);
|
|
1403
1434
|
const snippet = (res.body || '').slice(0, 300);
|
|
1404
1435
|
if (snippet) console.log(snippet);
|
|
1405
1436
|
}
|
|
1406
|
-
return { success, status: res.status, body: res.body, traceId, pairCode: pair };
|
|
1437
|
+
return { success, status: res.status, body: res.body, traceId, pairCode: pair, logid: res.logid || extractLogid(res.body || '') };
|
|
1407
1438
|
}
|
|
1408
1439
|
|
|
1409
1440
|
// ── OpenClaw 专属上报链路校验 ──────────────────────────────────────────────
|
|
@@ -1500,9 +1531,9 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
|
1500
1531
|
const tokenPrefix = authHeader.replace(/^Bearer\s+/i, '').slice(0, 12);
|
|
1501
1532
|
|
|
1502
1533
|
if (success) {
|
|
1503
|
-
ok(`openclaw 插件实际 token 上报正常 (token=${tokenPrefix}..., HTTP ${res.status})`);
|
|
1534
|
+
ok(`openclaw 插件实际 token 上报正常 (token=${tokenPrefix}..., HTTP ${res.status}${formatLogid(res.logid || '')})`);
|
|
1504
1535
|
} else {
|
|
1505
|
-
warn(`openclaw 插件实际 token 上报失败: HTTP ${res.status} (token=${tokenPrefix}...)`);
|
|
1536
|
+
warn(`openclaw 插件实际 token 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')} (token=${tokenPrefix}...)`);
|
|
1506
1537
|
const snippet = (res.body || '').slice(0, 300);
|
|
1507
1538
|
if (snippet) console.log(snippet);
|
|
1508
1539
|
// 4100/401 = 该 token 已失效。指出根因与修复方式。
|
|
@@ -1512,7 +1543,7 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
|
1512
1543
|
}
|
|
1513
1544
|
}
|
|
1514
1545
|
|
|
1515
|
-
return { success, status: res.status, body: res.body || '' };
|
|
1546
|
+
return { success, status: res.status, body: res.body || '', logid: res.logid || extractLogid(res.body || '') };
|
|
1516
1547
|
}
|
|
1517
1548
|
|
|
1518
1549
|
function httpsGet(url, headers) {
|
|
@@ -1745,6 +1776,7 @@ async function main() {
|
|
|
1745
1776
|
: await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
|
|
1746
1777
|
if (verifyResult.success) {
|
|
1747
1778
|
cloudResult.verify = 'ok';
|
|
1779
|
+
cloudResult.logid = verifyResult.logid || extractLogid(verifyResult.body) || cloudResult.logid;
|
|
1748
1780
|
} else if (CLOUD_MODE) {
|
|
1749
1781
|
// 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
|
|
1750
1782
|
cloudResult.verify = 'fail';
|
|
@@ -1760,6 +1792,7 @@ async function main() {
|
|
|
1760
1792
|
'ERROR: trace 上报自检失败',
|
|
1761
1793
|
'',
|
|
1762
1794
|
`HTTP ${verifyResult.status}`,
|
|
1795
|
+
`logid=${verifyResult.logid || extractLogid(verifyResult.body) || ''}`,
|
|
1763
1796
|
(verifyResult.body || '').slice(0, 300),
|
|
1764
1797
|
'',
|
|
1765
1798
|
'hook 配置已写入,但上报链路未打通。',
|
package/package.json
CHANGED
|
@@ -695,6 +695,32 @@ def _assistant_bounds(turn):
|
|
|
695
695
|
return min(values), max(values)
|
|
696
696
|
|
|
697
697
|
|
|
698
|
+
def extract_token_events(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
699
|
+
"""扫 rollout event_msg/token_count 事件, 与 turn 完全解耦.
|
|
700
|
+
|
|
701
|
+
每条事件独立采集, 依赖 _line_number 做全局水位线幂等. 只收 last_token_usage 非空的事件
|
|
702
|
+
(空的是心跳/占位, 不代表真实模型调用). total_token_usage 一并保留供 tag 上报.
|
|
703
|
+
"""
|
|
704
|
+
events: List[Dict[str, Any]] = []
|
|
705
|
+
for entry in entries:
|
|
706
|
+
if entry.get("type") != "event_msg":
|
|
707
|
+
continue
|
|
708
|
+
payload = entry.get("payload") or {}
|
|
709
|
+
if payload.get("type") != "token_count":
|
|
710
|
+
continue
|
|
711
|
+
info = payload.get("info") or {}
|
|
712
|
+
last_usage = info.get("last_token_usage")
|
|
713
|
+
if not isinstance(last_usage, dict) or not last_usage:
|
|
714
|
+
continue
|
|
715
|
+
events.append({
|
|
716
|
+
"line": entry.get("_line_number", 0),
|
|
717
|
+
"timestamp": entry.get("timestamp"),
|
|
718
|
+
"usage": last_usage,
|
|
719
|
+
"total_usage": info.get("total_token_usage") or {},
|
|
720
|
+
})
|
|
721
|
+
return events
|
|
722
|
+
|
|
723
|
+
|
|
698
724
|
def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
699
725
|
"""Group raw JSONL entries into conversation turns.
|
|
700
726
|
|
|
@@ -1361,7 +1387,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1361
1387
|
# tool span 挂到固定 root、当场 flush。codex 的实时单元是 tool_call(turn 内平铺,按 call_id 关联
|
|
1362
1388
|
# result)。全局水位线 state["rt_last_tool"] 跟踪已发的 tool_call 数。
|
|
1363
1389
|
|
|
1364
|
-
def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_override=None, is_terminal=False):
|
|
1390
|
+
def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_override=None, is_terminal=False, token_events=None):
|
|
1365
1391
|
import cozeloop
|
|
1366
1392
|
from cozeloop.spec.tracespec import Runtime
|
|
1367
1393
|
token = get_fresh_token()
|
|
@@ -1494,6 +1520,62 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1494
1520
|
if sent > 0:
|
|
1495
1521
|
state["rt_last_upload_ts"] = time.time()
|
|
1496
1522
|
|
|
1523
|
+
# --- token_count 独立增量, 与 tool span 同一 root 下, 按行号水位线幂等 ---
|
|
1524
|
+
# 每条 token_count 挂一个 model_usage_<line> span:
|
|
1525
|
+
# last_token_usage → set_input_tokens/set_output_tokens (SDK 官方求和字段)
|
|
1526
|
+
# total_token_usage → tags (只观察, 不参与后端聚合, 否则会重复累加)
|
|
1527
|
+
# 只在 flush 成功后推进 rt_last_token_line, 保证上报失败下次能重发.
|
|
1528
|
+
token_sent = 0
|
|
1529
|
+
last_token_line = state.get("rt_last_token_line", -1)
|
|
1530
|
+
pending_tokens = [ev for ev in (token_events or []) if ev["line"] > last_token_line]
|
|
1531
|
+
max_flushed_line = last_token_line
|
|
1532
|
+
for ev in pending_tokens:
|
|
1533
|
+
line = ev["line"]
|
|
1534
|
+
last = ev.get("usage") or {}
|
|
1535
|
+
total = ev.get("total_usage") or {}
|
|
1536
|
+
in_tk = _usage_int(last, "input_tokens")
|
|
1537
|
+
out_tk = _usage_int(last, "output_tokens")
|
|
1538
|
+
start_dt = _parse_ts_value(ev.get("timestamp"))
|
|
1539
|
+
tk_span = client.start_span(name=f"model_usage_{line}", span_type="model",
|
|
1540
|
+
start_time=start_dt, child_of=root_ctx)
|
|
1541
|
+
tk_span.set_runtime(Runtime(library="codex-cli"))
|
|
1542
|
+
try: tk_span.set_model_name(model_name)
|
|
1543
|
+
except Exception: pass
|
|
1544
|
+
if start_dt is not None:
|
|
1545
|
+
_set_finish_time_safe(tk_span, start_dt)
|
|
1546
|
+
if in_tk > 0:
|
|
1547
|
+
try: tk_span.set_input_tokens(in_tk)
|
|
1548
|
+
except Exception: pass
|
|
1549
|
+
if out_tk > 0:
|
|
1550
|
+
try: tk_span.set_output_tokens(out_tk)
|
|
1551
|
+
except Exception: pass
|
|
1552
|
+
tk_tags = {
|
|
1553
|
+
"token_event_line": line,
|
|
1554
|
+
"token_source": "codex_token_count",
|
|
1555
|
+
"cached_input_tokens": _usage_int(last, "cached_input_tokens"),
|
|
1556
|
+
"reasoning_output_tokens": _usage_int(last, "reasoning_output_tokens"),
|
|
1557
|
+
"total_tokens": _usage_int(last, "total_tokens"),
|
|
1558
|
+
}
|
|
1559
|
+
if total:
|
|
1560
|
+
tk_tags["total_input_tokens"] = _usage_int(total, "input_tokens")
|
|
1561
|
+
tk_tags["total_output_tokens"] = _usage_int(total, "output_tokens")
|
|
1562
|
+
tk_tags["total_cached_input_tokens"] = _usage_int(total, "cached_input_tokens")
|
|
1563
|
+
tk_tags["total_reasoning_output_tokens"] = _usage_int(total, "reasoning_output_tokens")
|
|
1564
|
+
tk_tags["total_all_tokens"] = _usage_int(total, "total_tokens")
|
|
1565
|
+
tk_span.set_tags(tk_tags)
|
|
1566
|
+
tk_span.finish()
|
|
1567
|
+
client.flush()
|
|
1568
|
+
if upload_events:
|
|
1569
|
+
hook_log(f"[rt] upload failed after token flush line={line} detail={upload_events[-1][:500]}")
|
|
1570
|
+
if max_flushed_line > last_token_line:
|
|
1571
|
+
state["rt_last_token_line"] = max_flushed_line
|
|
1572
|
+
return None
|
|
1573
|
+
max_flushed_line = line
|
|
1574
|
+
token_sent += 1
|
|
1575
|
+
if token_sent > 0:
|
|
1576
|
+
state["rt_last_token_line"] = max_flushed_line
|
|
1577
|
+
state["rt_last_upload_ts"] = time.time()
|
|
1578
|
+
|
|
1497
1579
|
if is_terminal:
|
|
1498
1580
|
# 收尾:补最终 assistant 输出
|
|
1499
1581
|
last_out = ""
|
|
@@ -1519,7 +1601,7 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1519
1601
|
hook_log(f"[rt] upload failed state not advanced detail={upload_events[-1][:500]}")
|
|
1520
1602
|
return None
|
|
1521
1603
|
|
|
1522
|
-
hook_log(f"[rt] sent {sent} tool span(s), last_tool={state['rt_last_tool']}, terminal={is_terminal}")
|
|
1604
|
+
hook_log(f"[rt] sent {sent} tool span(s), {token_sent} token span(s), last_tool={state['rt_last_tool']}, last_token_line={state.get('rt_last_token_line', -1)}, terminal={is_terminal}")
|
|
1523
1605
|
return True
|
|
1524
1606
|
except Exception as e:
|
|
1525
1607
|
hook_log(f"[rt] error: {e}")
|
|
@@ -1637,7 +1719,9 @@ def main():
|
|
|
1637
1719
|
|
|
1638
1720
|
# Group entries into turns
|
|
1639
1721
|
turns = group_messages_into_turns(entries)
|
|
1640
|
-
|
|
1722
|
+
# 独立扫 token_count 事件, 与 turn 解耦, 走独立行号水位线上报.
|
|
1723
|
+
token_events = extract_token_events(entries)
|
|
1724
|
+
debug_log(f"Grouped into {len(turns)} turns, {len(token_events)} token events")
|
|
1641
1725
|
|
|
1642
1726
|
# If this is a subagent, save data for parent to include later
|
|
1643
1727
|
if is_subagent:
|
|
@@ -1677,7 +1761,8 @@ def main():
|
|
|
1677
1761
|
# 方案 B 实时:把新完成的 tool_call 作为 span 挂稳定 root,结束即可见。不节流。
|
|
1678
1762
|
ok = send_steps_realtime(turns, session_id, state, model_name=model_name,
|
|
1679
1763
|
coze_tags_override=coze_tags_override,
|
|
1680
|
-
is_terminal=is_terminal_event
|
|
1764
|
+
is_terminal=is_terminal_event,
|
|
1765
|
+
token_events=token_events)
|
|
1681
1766
|
if ok is None:
|
|
1682
1767
|
hook_log(f"realtime send failed session_id={session_id}")
|
|
1683
1768
|
debug_log("Realtime send failed, state not advanced")
|
|
@@ -47,6 +47,48 @@ function fileLog(logFile, message) {
|
|
|
47
47
|
/* best-effort, never throw from logging */
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
function headerString(value) {
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return value.find(Boolean) || "";
|
|
53
|
+
return value ? String(value) : "";
|
|
54
|
+
}
|
|
55
|
+
function extractLogidFromHeaders(headers) {
|
|
56
|
+
if (!headers)
|
|
57
|
+
return "";
|
|
58
|
+
const candidates = [
|
|
59
|
+
"x-tt-logid",
|
|
60
|
+
"x-tt-log-id",
|
|
61
|
+
"x-log-id",
|
|
62
|
+
"x-logid",
|
|
63
|
+
"x-request-id",
|
|
64
|
+
"x-tt-trace-id",
|
|
65
|
+
"trace-id",
|
|
66
|
+
];
|
|
67
|
+
for (const key of candidates) {
|
|
68
|
+
const value = headerString(headers[key] || headers[key.toLowerCase()] || headers[key.toUpperCase()]);
|
|
69
|
+
if (value)
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
function extractLogid(text) {
|
|
75
|
+
if (!text)
|
|
76
|
+
return "";
|
|
77
|
+
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
78
|
+
if (m)
|
|
79
|
+
return m[1];
|
|
80
|
+
m = text.match(/"log_id"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
81
|
+
if (m)
|
|
82
|
+
return m[1];
|
|
83
|
+
m = text.match(/"Logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
84
|
+
if (m)
|
|
85
|
+
return m[1];
|
|
86
|
+
m = text.match(/logid[=:\s]+([a-zA-Z0-9._:-]+)/i);
|
|
87
|
+
return m ? m[1] : "";
|
|
88
|
+
}
|
|
89
|
+
function formatLogid(logid) {
|
|
90
|
+
return logid ? ` logid=${logid}` : " logid=";
|
|
91
|
+
}
|
|
50
92
|
|
|
51
93
|
function normalizeApiBaseUrl(endpoint) {
|
|
52
94
|
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
@@ -215,7 +257,12 @@ function postJson(url, body, headers) {
|
|
|
215
257
|
}, (res) => {
|
|
216
258
|
let buf = "";
|
|
217
259
|
res.on("data", c => buf += c);
|
|
218
|
-
res.on("end", () => resolve({
|
|
260
|
+
res.on("end", () => resolve({
|
|
261
|
+
status: res.statusCode || 0,
|
|
262
|
+
body: buf,
|
|
263
|
+
headers: res.headers || {},
|
|
264
|
+
logid: extractLogidFromHeaders(res.headers) || extractLogid(buf),
|
|
265
|
+
}));
|
|
219
266
|
});
|
|
220
267
|
req.on("error", reject);
|
|
221
268
|
req.setTimeout(15000, () => req.destroy(new Error("CozeLoop ingest export timed out")));
|
|
@@ -305,8 +352,8 @@ class CozeloopIngestExporter {
|
|
|
305
352
|
const res = await postJson(this.url, body, this.headers);
|
|
306
353
|
if (res.status < 200 || res.status >= 300) {
|
|
307
354
|
const snippet = String(res.body || "").slice(0, 300);
|
|
308
|
-
fileLog(this.logFile, `[ingest] HTTP ${res.status} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
|
|
309
|
-
throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
|
|
355
|
+
fileLog(this.logFile, `[ingest] HTTP ${res.status}${formatLogid(res.logid)} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
|
|
356
|
+
throw new Error(`HTTP ${res.status}${formatLogid(res.logid)}${snippet ? `: ${snippet}` : ""}`);
|
|
310
357
|
}
|
|
311
358
|
if (res.body) {
|
|
312
359
|
try {
|
|
@@ -322,7 +369,7 @@ class CozeloopIngestExporter {
|
|
|
322
369
|
}
|
|
323
370
|
}
|
|
324
371
|
}
|
|
325
|
-
fileLog(this.logFile, `[ingest] OK HTTP ${res.status} spans=${body.spans.length}${retry ? " retry=1" : ""}`);
|
|
372
|
+
fileLog(this.logFile, `[ingest] OK HTTP ${res.status}${formatLogid(res.logid)} spans=${body.spans.length}${retry ? " retry=1" : ""}`);
|
|
326
373
|
}
|
|
327
374
|
async forceFlush() {
|
|
328
375
|
// Wait for every in-flight POST to settle. allSettled (not all) so one
|
|
@@ -196,6 +196,54 @@ function formatAssistantOutput(content, stopReason) {
|
|
|
196
196
|
],
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
+
function usageInt(usage, keys) {
|
|
200
|
+
if (!usage || typeof usage !== "object")
|
|
201
|
+
return 0;
|
|
202
|
+
for (const key of keys) {
|
|
203
|
+
const value = usage[key];
|
|
204
|
+
if (value === undefined || value === null || value === "")
|
|
205
|
+
continue;
|
|
206
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
207
|
+
if (Number.isFinite(n) && n > 0)
|
|
208
|
+
return Math.trunc(n);
|
|
209
|
+
}
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
function readTokenUsage(...usages) {
|
|
213
|
+
const inputKeys = [
|
|
214
|
+
"input",
|
|
215
|
+
"input_tokens",
|
|
216
|
+
"inputTokens",
|
|
217
|
+
"prompt_tokens",
|
|
218
|
+
"promptTokens",
|
|
219
|
+
"prompt",
|
|
220
|
+
];
|
|
221
|
+
const outputKeys = [
|
|
222
|
+
"output",
|
|
223
|
+
"output_tokens",
|
|
224
|
+
"outputTokens",
|
|
225
|
+
"completion_tokens",
|
|
226
|
+
"completionTokens",
|
|
227
|
+
"completion",
|
|
228
|
+
];
|
|
229
|
+
const totalKeys = ["total", "total_tokens", "totalTokens"];
|
|
230
|
+
let input = 0;
|
|
231
|
+
let output = 0;
|
|
232
|
+
let total = 0;
|
|
233
|
+
for (const usage of usages) {
|
|
234
|
+
if (!usage || typeof usage !== "object")
|
|
235
|
+
continue;
|
|
236
|
+
if (input === 0)
|
|
237
|
+
input = usageInt(usage, inputKeys);
|
|
238
|
+
if (output === 0)
|
|
239
|
+
output = usageInt(usage, outputKeys);
|
|
240
|
+
if (total === 0)
|
|
241
|
+
total = usageInt(usage, totalKeys);
|
|
242
|
+
if (input > 0 && output > 0 && total > 0)
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
return { input, output, total };
|
|
246
|
+
}
|
|
199
247
|
function convertAssistantContentForMessages(content) {
|
|
200
248
|
if (!Array.isArray(content))
|
|
201
249
|
return [{ type: "text", text: String(content ?? "") }];
|
|
@@ -901,11 +949,15 @@ const cozeloopTracePlugin = {
|
|
|
901
949
|
const model = entry.model || ctx.lastModelId || "unknown";
|
|
902
950
|
const spanStartTime = prevWrittenAt;
|
|
903
951
|
const spanEndTime = entryWrittenAt;
|
|
952
|
+
const tokenUsage = readTokenUsage(entry.usage);
|
|
904
953
|
const modelSpan = createSpan(ctx, channelId, `${provider}/${model}`, "model", spanStartTime, spanEndTime, {
|
|
905
954
|
"gen_ai.provider.name": provider,
|
|
906
955
|
"gen_ai.request.model": model,
|
|
907
|
-
"gen_ai.usage.input_tokens":
|
|
908
|
-
"gen_ai.usage.output_tokens":
|
|
956
|
+
"gen_ai.usage.input_tokens": tokenUsage.input,
|
|
957
|
+
"gen_ai.usage.output_tokens": tokenUsage.output,
|
|
958
|
+
"gen_ai.usage.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
|
|
959
|
+
"input_tokens": tokenUsage.input,
|
|
960
|
+
"output_tokens": tokenUsage.output,
|
|
909
961
|
"react_round": reactRound,
|
|
910
962
|
}, { messages: reactMessages.map((msg) => safeClone(msg)) }, formatAssistantOutput(entry.content, entry.stopReason));
|
|
911
963
|
await exporter.export(modelSpan);
|
|
@@ -1269,13 +1321,15 @@ const cozeloopTracePlugin = {
|
|
|
1269
1321
|
}
|
|
1270
1322
|
}
|
|
1271
1323
|
const lastAssistantUsage = event.lastAssistant?.usage;
|
|
1272
|
-
const
|
|
1273
|
-
const outputTokens = event.usage?.output ?? lastAssistantUsage?.output ?? 0;
|
|
1324
|
+
const tokenUsage = readTokenUsage(event.usage, lastAssistantUsage);
|
|
1274
1325
|
const spanAttributes = {
|
|
1275
1326
|
"gen_ai.provider.name": event.provider,
|
|
1276
1327
|
"gen_ai.request.model": event.model,
|
|
1277
|
-
"gen_ai.usage.input_tokens":
|
|
1278
|
-
"gen_ai.usage.output_tokens":
|
|
1328
|
+
"gen_ai.usage.input_tokens": tokenUsage.input,
|
|
1329
|
+
"gen_ai.usage.output_tokens": tokenUsage.output,
|
|
1330
|
+
"gen_ai.usage.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
|
|
1331
|
+
"input_tokens": tokenUsage.input,
|
|
1332
|
+
"output_tokens": tokenUsage.output,
|
|
1279
1333
|
};
|
|
1280
1334
|
const finalOutput = formatAssistantOutput(event.assistantTexts?.map((t) => ({ type: "text", text: t })) ?? [], "stop");
|
|
1281
1335
|
const span = createSpan(ctx, channelId, `${event.provider}/${event.model}`, "model", startTime, now, spanAttributes, llmInput, finalOutput);
|
|
@@ -1595,11 +1649,12 @@ const cozeloopTracePlugin = {
|
|
|
1595
1649
|
const active = resolveActiveAgentContext(hookCtx, rawChannelId);
|
|
1596
1650
|
const ctx = active?.ctx || getOrCreateContext(rawChannelId, undefined, "agent_end").ctx;
|
|
1597
1651
|
const channelId = active?.channelId || rawChannelId;
|
|
1652
|
+
const tokenUsage = readTokenUsage(event.usage);
|
|
1598
1653
|
finalizeTrace(ctx, channelId, {
|
|
1599
1654
|
"agent.duration_ms": event.durationMs || 0,
|
|
1600
1655
|
"agent.message_count": event.messageCount || 0,
|
|
1601
1656
|
"agent.tool_call_count": event.toolCallCount || 0,
|
|
1602
|
-
"agent.total_tokens":
|
|
1657
|
+
"agent.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
|
|
1603
1658
|
}, { usage: event.usage, cost: event.cost });
|
|
1604
1659
|
});
|
|
1605
1660
|
}
|