coze_lab 0.1.54 → 0.1.56
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/package.json
CHANGED
|
@@ -1465,6 +1465,33 @@ def _flatten_completed_steps(turns: List[Dict[str, Any]], is_terminal: bool = Fa
|
|
|
1465
1465
|
return completed, len(flat)
|
|
1466
1466
|
|
|
1467
1467
|
|
|
1468
|
+
def _reconstructed_model_bounds(item: Dict[str, Any]):
|
|
1469
|
+
"""Return request-ready -> assistant-emitted bounds for one Claude step."""
|
|
1470
|
+
turn = item.get("turn") or {}
|
|
1471
|
+
step = item.get("step") or {}
|
|
1472
|
+
step_index = item.get("step_index", 0)
|
|
1473
|
+
assistant_dt = _parse_ts(step.get("assistant_message"))
|
|
1474
|
+
start_dt = None
|
|
1475
|
+
if step_index > 0:
|
|
1476
|
+
previous_steps = turn.get("steps", [])
|
|
1477
|
+
if step_index <= len(previous_steps) - 1:
|
|
1478
|
+
previous = previous_steps[step_index - 1]
|
|
1479
|
+
result_times = [
|
|
1480
|
+
_parse_ts_str(result.get("_result_ts"))
|
|
1481
|
+
for result in previous.get("tool_results", [])
|
|
1482
|
+
]
|
|
1483
|
+
result_times = [value for value in result_times if value is not None]
|
|
1484
|
+
if result_times:
|
|
1485
|
+
start_dt = max(result_times)
|
|
1486
|
+
else:
|
|
1487
|
+
start_dt = _parse_ts(previous.get("assistant_message"))
|
|
1488
|
+
if start_dt is None:
|
|
1489
|
+
start_dt = _parse_ts(turn.get("user_message"))
|
|
1490
|
+
if start_dt is None:
|
|
1491
|
+
start_dt = assistant_dt
|
|
1492
|
+
return start_dt, assistant_dt or start_dt
|
|
1493
|
+
|
|
1494
|
+
|
|
1468
1495
|
def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_override=None, is_terminal=False):
|
|
1469
1496
|
"""实时上报:把新完成的 step 作为 span 挂到稳定 root,结束即 flush。
|
|
1470
1497
|
|
|
@@ -1569,17 +1596,14 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
|
|
|
1569
1596
|
amo = assistant_msg.get("message", {})
|
|
1570
1597
|
model_name = amo.get("model", "claude-code")
|
|
1571
1598
|
gidx = last_global + sent # 全局 step 序号,用于命名
|
|
1572
|
-
step_start_dt =
|
|
1573
|
-
|
|
1574
|
-
for r in step.get("tool_results", []):
|
|
1575
|
-
te = _parse_ts_str(r.get("_result_ts"))
|
|
1576
|
-
if te:
|
|
1577
|
-
step_end_dt = te
|
|
1599
|
+
step_start_dt, step_end_dt = _reconstructed_model_bounds(item)
|
|
1600
|
+
assistant_dt = _parse_ts(assistant_msg) or step_end_dt
|
|
1578
1601
|
|
|
1579
1602
|
# model_span 挂 root
|
|
1580
1603
|
mspan = client.start_span(name=f"model_call_{gidx}", span_type="model",
|
|
1581
1604
|
start_time=step_start_dt, child_of=root_ctx)
|
|
1582
1605
|
mspan.set_runtime(Runtime(library="claude-code"))
|
|
1606
|
+
mspan.set_tags({"timing_source": "reconstructed"})
|
|
1583
1607
|
try:
|
|
1584
1608
|
mspan.set_model_name(model_name)
|
|
1585
1609
|
except Exception:
|
|
@@ -1635,7 +1659,7 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
|
|
|
1635
1659
|
tool_end_dt = _parse_ts_str(r.get("_result_ts"))
|
|
1636
1660
|
break
|
|
1637
1661
|
tspan = client.start_span(name=f"tool_{tname}", span_type="tool",
|
|
1638
|
-
start_time=
|
|
1662
|
+
start_time=assistant_dt, child_of=mspan_ctx)
|
|
1639
1663
|
tspan.set_runtime(Runtime(library="claude-code"))
|
|
1640
1664
|
if tool_end_dt is not None:
|
|
1641
1665
|
_set_finish_time_safe(tspan, tool_end_dt)
|
|
@@ -741,13 +741,45 @@ def _assistant_bounds(turn):
|
|
|
741
741
|
return min(values), max(values)
|
|
742
742
|
|
|
743
743
|
|
|
744
|
-
def
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
744
|
+
def extract_model_invocations(turns: List[Dict[str, Any]],
|
|
745
|
+
entries: List[Dict[str, Any]],
|
|
746
|
+
is_terminal: bool = False) -> List[Dict[str, Any]]:
|
|
747
|
+
"""Group visible model output, tool calls, and usage into model invocations.
|
|
748
|
+
|
|
749
|
+
Codex emits one non-empty ``token_count.last_token_usage`` after each model
|
|
750
|
+
invocation. It is the only reliable boundary shared by tool and no-tool
|
|
751
|
+
calls, so records since the previous boundary are reported together. The
|
|
752
|
+
cumulative ``total_token_usage`` is retained as metadata only.
|
|
749
753
|
"""
|
|
750
754
|
events: List[Dict[str, Any]] = []
|
|
755
|
+
tool_results: Dict[str, Dict[str, Any]] = {}
|
|
756
|
+
|
|
757
|
+
for turn in turns:
|
|
758
|
+
for result in turn.get("tool_results", []):
|
|
759
|
+
call_id = result.get("call_id")
|
|
760
|
+
if call_id:
|
|
761
|
+
tool_results[call_id] = result
|
|
762
|
+
for record in turn.get("model_records", []):
|
|
763
|
+
events.append({
|
|
764
|
+
"kind": "record",
|
|
765
|
+
"line": record.get("_line_number", -1),
|
|
766
|
+
"timestamp": record.get("_ts"),
|
|
767
|
+
"record": record,
|
|
768
|
+
"turn": turn,
|
|
769
|
+
})
|
|
770
|
+
for tool_call in turn.get("tool_calls", []):
|
|
771
|
+
events.append({
|
|
772
|
+
"kind": "tool",
|
|
773
|
+
"line": tool_call.get("_line_number", -1),
|
|
774
|
+
"timestamp": tool_call.get("_ts"),
|
|
775
|
+
"tool_call": tool_call,
|
|
776
|
+
"turn": turn,
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
turn_starts = sorted(
|
|
780
|
+
((turn.get("start_line", -1), turn) for turn in turns),
|
|
781
|
+
key=lambda item: item[0],
|
|
782
|
+
)
|
|
751
783
|
for entry in entries:
|
|
752
784
|
if entry.get("type") != "event_msg":
|
|
753
785
|
continue
|
|
@@ -758,13 +790,132 @@ def extract_token_events(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
|
758
790
|
last_usage = info.get("last_token_usage")
|
|
759
791
|
if not isinstance(last_usage, dict) or not last_usage:
|
|
760
792
|
continue
|
|
793
|
+
line = entry.get("_line_number", 0)
|
|
794
|
+
boundary_turn = None
|
|
795
|
+
for start_line, turn in turn_starts:
|
|
796
|
+
if start_line > line:
|
|
797
|
+
break
|
|
798
|
+
boundary_turn = turn
|
|
761
799
|
events.append({
|
|
762
|
-
"
|
|
800
|
+
"kind": "usage",
|
|
801
|
+
"line": line,
|
|
763
802
|
"timestamp": entry.get("timestamp"),
|
|
764
803
|
"usage": last_usage,
|
|
765
804
|
"total_usage": info.get("total_token_usage") or {},
|
|
805
|
+
"turn": boundary_turn,
|
|
766
806
|
})
|
|
767
|
-
|
|
807
|
+
|
|
808
|
+
events.sort(key=lambda item: (item.get("line", -1),
|
|
809
|
+
{"record": 0, "tool": 1, "usage": 2}.get(item.get("kind"), 3)))
|
|
810
|
+
invocations: List[Dict[str, Any]] = []
|
|
811
|
+
current: Dict[str, Any] = {
|
|
812
|
+
"content_parts": [],
|
|
813
|
+
"reasoning_parts": [],
|
|
814
|
+
"protected": {"source": "", "count": 0, "bytes": 0},
|
|
815
|
+
"tool_calls": [],
|
|
816
|
+
"turn": None,
|
|
817
|
+
"start_timestamp": None,
|
|
818
|
+
"end_timestamp": None,
|
|
819
|
+
"line": -1,
|
|
820
|
+
"usage": {},
|
|
821
|
+
"total_usage": {},
|
|
822
|
+
}
|
|
823
|
+
seen = {"content": set(), "reasoning": set()}
|
|
824
|
+
|
|
825
|
+
def request_timestamp(turn: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
826
|
+
if not turn:
|
|
827
|
+
return None
|
|
828
|
+
user_message = turn.get("user_message") or {}
|
|
829
|
+
return user_message.get("_ts") or turn.get("start_timestamp")
|
|
830
|
+
|
|
831
|
+
def append_text(category: str, value: Any):
|
|
832
|
+
if not isinstance(value, str):
|
|
833
|
+
return
|
|
834
|
+
text = value.strip()
|
|
835
|
+
if not text or text in seen[category]:
|
|
836
|
+
return
|
|
837
|
+
seen[category].add(text)
|
|
838
|
+
current[f"{category}_parts"].append(text)
|
|
839
|
+
|
|
840
|
+
def finish_current(boundary_line: int, boundary_ts: Optional[str],
|
|
841
|
+
usage: Optional[Dict[str, Any]] = None,
|
|
842
|
+
total_usage: Optional[Dict[str, Any]] = None):
|
|
843
|
+
has_payload = (
|
|
844
|
+
current["content_parts"] or current["reasoning_parts"]
|
|
845
|
+
or current["tool_calls"] or current["protected"].get("count")
|
|
846
|
+
or usage
|
|
847
|
+
)
|
|
848
|
+
if not has_payload:
|
|
849
|
+
return
|
|
850
|
+
current["line"] = boundary_line
|
|
851
|
+
if current["end_timestamp"] is None:
|
|
852
|
+
current["end_timestamp"] = boundary_ts
|
|
853
|
+
current["timing_source"] = "reconstructed_usage_fallback"
|
|
854
|
+
else:
|
|
855
|
+
current["timing_source"] = "reconstructed"
|
|
856
|
+
current["usage"] = usage or {}
|
|
857
|
+
current["total_usage"] = total_usage or {}
|
|
858
|
+
current["content"] = "\n".join(current.pop("content_parts"))
|
|
859
|
+
current["reasoning"] = "\n".join(current.pop("reasoning_parts"))
|
|
860
|
+
for tool_call in current["tool_calls"]:
|
|
861
|
+
tool_call["result"] = tool_results.get(tool_call.get("call_id"))
|
|
862
|
+
invocations.append(dict(current))
|
|
863
|
+
|
|
864
|
+
for event in events:
|
|
865
|
+
kind = event["kind"]
|
|
866
|
+
if kind == "usage":
|
|
867
|
+
if current["turn"] is None:
|
|
868
|
+
current["turn"] = event.get("turn")
|
|
869
|
+
if current["start_timestamp"] is None:
|
|
870
|
+
current["start_timestamp"] = event.get("timestamp")
|
|
871
|
+
finish_current(event["line"], event.get("timestamp"),
|
|
872
|
+
event.get("usage"), event.get("total_usage"))
|
|
873
|
+
current = {
|
|
874
|
+
"content_parts": [],
|
|
875
|
+
"reasoning_parts": [],
|
|
876
|
+
"protected": {"source": "", "count": 0, "bytes": 0},
|
|
877
|
+
"tool_calls": [],
|
|
878
|
+
"turn": None,
|
|
879
|
+
"start_timestamp": event.get("timestamp"),
|
|
880
|
+
"start_turn": event.get("turn"),
|
|
881
|
+
"end_timestamp": None,
|
|
882
|
+
"line": -1,
|
|
883
|
+
"usage": {},
|
|
884
|
+
"total_usage": {},
|
|
885
|
+
}
|
|
886
|
+
seen = {"content": set(), "reasoning": set()}
|
|
887
|
+
continue
|
|
888
|
+
|
|
889
|
+
current["turn"] = current["turn"] or event.get("turn")
|
|
890
|
+
if current.get("start_turn") is not None and current.get("start_turn") is not event.get("turn"):
|
|
891
|
+
current["start_timestamp"] = None
|
|
892
|
+
current["start_turn"] = event.get("turn")
|
|
893
|
+
current["start_timestamp"] = (
|
|
894
|
+
current["start_timestamp"]
|
|
895
|
+
or request_timestamp(event.get("turn"))
|
|
896
|
+
or event.get("timestamp")
|
|
897
|
+
)
|
|
898
|
+
current["end_timestamp"] = event.get("timestamp") or current["end_timestamp"]
|
|
899
|
+
current["line"] = max(current["line"], event.get("line", -1))
|
|
900
|
+
if kind == "record":
|
|
901
|
+
record = event["record"]
|
|
902
|
+
append_text("content", record.get("content"))
|
|
903
|
+
append_text("reasoning", record.get("reasoning"))
|
|
904
|
+
protected = record.get("protected") or {}
|
|
905
|
+
if protected.get("count"):
|
|
906
|
+
current["protected"]["source"] = protected.get("source", "")
|
|
907
|
+
current["protected"]["count"] += protected.get("count", 0)
|
|
908
|
+
current["protected"]["bytes"] += protected.get("bytes", 0)
|
|
909
|
+
elif kind == "tool":
|
|
910
|
+
current["tool_calls"].append(dict(event["tool_call"]))
|
|
911
|
+
|
|
912
|
+
if is_terminal and (
|
|
913
|
+
current["content_parts"] or current["reasoning_parts"]
|
|
914
|
+
or current["tool_calls"] or current["protected"].get("count")
|
|
915
|
+
):
|
|
916
|
+
finish_current(current["line"], current["end_timestamp"])
|
|
917
|
+
|
|
918
|
+
return invocations
|
|
768
919
|
|
|
769
920
|
|
|
770
921
|
def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
@@ -811,6 +962,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
811
962
|
"subagent_calls": [],
|
|
812
963
|
"token_usage": {},
|
|
813
964
|
"start_line": entry.get("_line_number", 0),
|
|
965
|
+
"start_timestamp": payload.get("_ts"),
|
|
814
966
|
}
|
|
815
967
|
pending_calls = {}
|
|
816
968
|
elif msg_type == "task_complete":
|
|
@@ -923,6 +1075,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
923
1075
|
"input": args,
|
|
924
1076
|
"custom": item_type == "custom_tool_call",
|
|
925
1077
|
"_ts": payload.get("_ts"),
|
|
1078
|
+
"_line_number": entry.get("_line_number", -1),
|
|
926
1079
|
})
|
|
927
1080
|
pending_calls[call_id] = {"kind": "tool", "_start_ts": payload.get("_ts")}
|
|
928
1081
|
|
|
@@ -1481,14 +1634,15 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1481
1634
|
return ctx
|
|
1482
1635
|
|
|
1483
1636
|
|
|
1484
|
-
# --- 实时
|
|
1637
|
+
# --- 实时 model invocation 上报 -------------------------------------------
|
|
1485
1638
|
#
|
|
1486
1639
|
# 与 claude-code 同架构:一个会话 = 一条稳定 trace。首次建 root(codex_request)立即 finish 并存
|
|
1487
|
-
# to_header() 到 state["rt_root_header"]
|
|
1488
|
-
#
|
|
1489
|
-
#
|
|
1640
|
+
# to_header() 到 state["rt_root_header"];之后按 token_count 边界把模型可见内容、推理、工具调用和
|
|
1641
|
+
# last_token_usage 合成 model_call span,工具执行作为其子 span。全局行号水位线
|
|
1642
|
+
# state["rt_last_invocation_line"] 保证重复 hook 不重发。
|
|
1490
1643
|
|
|
1491
|
-
def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_override=None,
|
|
1644
|
+
def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_override=None,
|
|
1645
|
+
is_terminal=False, model_invocations=None):
|
|
1492
1646
|
import cozeloop
|
|
1493
1647
|
from cozeloop.spec.tracespec import Runtime
|
|
1494
1648
|
token = get_fresh_token()
|
|
@@ -1562,197 +1716,163 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1562
1716
|
return None
|
|
1563
1717
|
hook_log(f"[rt] root created trace_id={getattr(root_ctx,'trace_id','?')}")
|
|
1564
1718
|
|
|
1565
|
-
#
|
|
1566
|
-
#
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1719
|
+
# One atomic model span per Codex invocation. Upgrade existing sessions
|
|
1720
|
+
# without replaying events already emitted by the pre-0.1.55 watermarks.
|
|
1721
|
+
legacy_line = max(state.get("rt_last_model_line", -1),
|
|
1722
|
+
state.get("rt_last_token_line", -1))
|
|
1723
|
+
last_invocation_line = state.get("rt_last_invocation_line", legacy_line)
|
|
1724
|
+
pending_invocations = [
|
|
1725
|
+
invocation for invocation in (model_invocations or [])
|
|
1726
|
+
if invocation.get("line", -1) > last_invocation_line
|
|
1727
|
+
]
|
|
1728
|
+
if pending_invocations and not is_terminal:
|
|
1729
|
+
since = time.time() - state.get("rt_last_upload_ts", 0)
|
|
1730
|
+
if since < REALTIME_BATCH_INTERVAL:
|
|
1731
|
+
hook_log(
|
|
1732
|
+
f"[rt] batch throttle {since:.1f}s<{REALTIME_BATCH_INTERVAL}s "
|
|
1733
|
+
f"defer {len(pending_invocations)} invocation(s)"
|
|
1734
|
+
)
|
|
1735
|
+
pending_invocations = []
|
|
1736
|
+
|
|
1737
|
+
invocation_sent = 0
|
|
1738
|
+
tool_sent = 0
|
|
1739
|
+
model_call_base = state.get(
|
|
1740
|
+
"rt_model_call_count", state.get("rt_last_tool", 0)
|
|
1741
|
+
)
|
|
1742
|
+
for invocation in pending_invocations:
|
|
1743
|
+
line = invocation.get("line", -1)
|
|
1744
|
+
turn = invocation.get("turn") or {}
|
|
1745
|
+
tool_calls = invocation.get("tool_calls") or []
|
|
1746
|
+
model_tool_calls = []
|
|
1747
|
+
for tool_call in tool_calls:
|
|
1748
|
+
model_tool_calls.append(ModelToolCall(
|
|
1749
|
+
id=tool_call.get("call_id", ""),
|
|
1750
|
+
type="function",
|
|
1751
|
+
function=ModelToolCallFunction(
|
|
1752
|
+
name=tool_call.get("name", ""),
|
|
1753
|
+
arguments=_json_text(tool_call.get("input", {})),
|
|
1754
|
+
),
|
|
1755
|
+
))
|
|
1756
|
+
|
|
1757
|
+
start_dt = _parse_ts_value(invocation.get("start_timestamp"))
|
|
1758
|
+
end_dt = _parse_ts_value(invocation.get("end_timestamp")) or start_dt
|
|
1587
1759
|
mspan = client.start_span(
|
|
1588
|
-
name=f"
|
|
1589
|
-
start_time=start_dt, child_of=root_ctx,
|
|
1760
|
+
name=f"model_call_{invocation_sent + model_call_base}",
|
|
1761
|
+
span_type="model", start_time=start_dt, child_of=root_ctx,
|
|
1590
1762
|
)
|
|
1591
1763
|
mspan.set_runtime(Runtime(library="codex-cli"))
|
|
1592
1764
|
try:
|
|
1593
1765
|
mspan.set_model_name(model_name)
|
|
1594
1766
|
except Exception:
|
|
1595
1767
|
pass
|
|
1596
|
-
if
|
|
1597
|
-
mspan
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1768
|
+
if start_dt is not None and end_dt is not None:
|
|
1769
|
+
_set_finish_time_safe(mspan, _max_dt(start_dt, end_dt))
|
|
1770
|
+
|
|
1771
|
+
input_messages = []
|
|
1772
|
+
for message in turn.get("input_messages", []):
|
|
1773
|
+
input_messages.append(_make_model_message(
|
|
1774
|
+
message.get("role", "user"), message.get("content", "")
|
|
1601
1775
|
))
|
|
1602
|
-
|
|
1603
|
-
|
|
1776
|
+
if not input_messages and turn.get("user_message_text"):
|
|
1777
|
+
input_messages.append(_make_model_message("user", turn["user_message_text"]))
|
|
1778
|
+
mspan.set_input(ModelInput(
|
|
1779
|
+
messages=input_messages,
|
|
1780
|
+
tools=[],
|
|
1781
|
+
tool_choice=ModelToolChoice(type="", function=None),
|
|
1782
|
+
))
|
|
1783
|
+
mspan.set_output(ModelOutput(choices=[ModelChoice(
|
|
1784
|
+
finish_reason="tool_calls" if model_tool_calls else "stop",
|
|
1604
1785
|
index=0,
|
|
1605
1786
|
message=ModelMessage(
|
|
1606
1787
|
role="assistant",
|
|
1607
|
-
content=content,
|
|
1608
|
-
reasoning_content=reasoning,
|
|
1788
|
+
content=invocation.get("content", ""),
|
|
1789
|
+
reasoning_content=invocation.get("reasoning", ""),
|
|
1609
1790
|
parts=[],
|
|
1610
1791
|
name="",
|
|
1611
|
-
tool_calls=
|
|
1792
|
+
tool_calls=model_tool_calls,
|
|
1612
1793
|
tool_call_id="",
|
|
1613
1794
|
metadata={},
|
|
1614
1795
|
),
|
|
1615
|
-
)
|
|
1616
|
-
|
|
1796
|
+
)]))
|
|
1797
|
+
|
|
1798
|
+
usage = invocation.get("usage") or {}
|
|
1799
|
+
in_tk = _usage_int(usage, "input_tokens")
|
|
1800
|
+
out_tk = _usage_int(usage, "output_tokens")
|
|
1801
|
+
if in_tk > 0:
|
|
1802
|
+
try:
|
|
1803
|
+
mspan.set_input_tokens(in_tk)
|
|
1804
|
+
except Exception:
|
|
1805
|
+
pass
|
|
1806
|
+
if out_tk > 0:
|
|
1807
|
+
try:
|
|
1808
|
+
mspan.set_output_tokens(out_tk)
|
|
1809
|
+
except Exception:
|
|
1810
|
+
pass
|
|
1811
|
+
|
|
1617
1812
|
tags = {
|
|
1618
|
-
"
|
|
1619
|
-
"
|
|
1813
|
+
"invocation_boundary_line": line,
|
|
1814
|
+
"token_source": "codex_token_count" if usage else "unavailable",
|
|
1815
|
+
"timing_source": invocation.get("timing_source", "reconstructed"),
|
|
1816
|
+
"cached_input_tokens": _usage_int(usage, "cached_input_tokens"),
|
|
1817
|
+
"reasoning_output_tokens": _usage_int(usage, "reasoning_output_tokens"),
|
|
1818
|
+
"total_tokens": _usage_int(usage, "total_tokens"),
|
|
1620
1819
|
}
|
|
1621
|
-
|
|
1820
|
+
total = invocation.get("total_usage") or {}
|
|
1821
|
+
if total:
|
|
1822
|
+
tags.update({
|
|
1823
|
+
"total_input_tokens": _usage_int(total, "input_tokens"),
|
|
1824
|
+
"total_output_tokens": _usage_int(total, "output_tokens"),
|
|
1825
|
+
"total_cached_input_tokens": _usage_int(total, "cached_input_tokens"),
|
|
1826
|
+
"total_reasoning_output_tokens": _usage_int(total, "reasoning_output_tokens"),
|
|
1827
|
+
"total_all_tokens": _usage_int(total, "total_tokens"),
|
|
1828
|
+
})
|
|
1829
|
+
protected = invocation.get("protected") or {}
|
|
1830
|
+
if protected.get("count"):
|
|
1622
1831
|
tags.update({
|
|
1623
1832
|
"protected_reasoning_source": protected.get("source", ""),
|
|
1624
1833
|
"protected_reasoning_count": protected.get("count", 0),
|
|
1625
1834
|
"protected_reasoning_bytes": protected.get("bytes", 0),
|
|
1626
1835
|
})
|
|
1627
1836
|
mspan.set_tags(tags)
|
|
1628
|
-
if start_dt is not None:
|
|
1629
|
-
_set_finish_time_safe(mspan, start_dt)
|
|
1630
|
-
mspan.finish()
|
|
1631
|
-
client.flush()
|
|
1632
|
-
if upload_events:
|
|
1633
|
-
hook_log(f"[rt] upload failed after model flush line={line} detail={upload_events[-1][:500]}")
|
|
1634
|
-
if max_model_line > last_model_line:
|
|
1635
|
-
state["rt_last_model_line"] = max_model_line
|
|
1636
|
-
return None
|
|
1637
|
-
max_model_line = line
|
|
1638
|
-
model_sent += 1
|
|
1639
|
-
if max_model_line > last_model_line:
|
|
1640
|
-
state["rt_last_model_line"] = max_model_line
|
|
1641
|
-
|
|
1642
|
-
# 线性展开所有 tool_call(带 result 的才算完成),按全局序增量发
|
|
1643
|
-
flat = []
|
|
1644
|
-
for ti, turn in enumerate(turns):
|
|
1645
|
-
results = {r.get("call_id"): r for r in turn.get("tool_results", [])}
|
|
1646
|
-
for tc in turn.get("tool_calls", []):
|
|
1647
|
-
cid = tc.get("call_id")
|
|
1648
|
-
flat.append({"tc": tc, "result": results.get(cid), "turn_index": ti})
|
|
1649
|
-
# 只发已完成(有 result)的,且全局最后一个若无 result 则留到下次
|
|
1650
|
-
completed = []
|
|
1651
|
-
for idx, item in enumerate(flat):
|
|
1652
|
-
is_last = (idx == len(flat) - 1)
|
|
1653
|
-
if item["result"] is not None or (not is_last):
|
|
1654
|
-
completed.append(item)
|
|
1655
|
-
last_tool = state.get("rt_last_tool", 0)
|
|
1656
|
-
new_items = completed[last_tool:]
|
|
1657
|
-
# 批量节流:非终态时距上次上报<5s 则本次不发,攒到下次一起发。终态不节流。
|
|
1658
|
-
if new_items and not is_terminal:
|
|
1659
|
-
since = time.time() - state.get("rt_last_upload_ts", 0)
|
|
1660
|
-
if since < REALTIME_BATCH_INTERVAL:
|
|
1661
|
-
hook_log(f"[rt] batch throttle {since:.1f}s<{REALTIME_BATCH_INTERVAL}s defer {len(new_items)} tool(s)")
|
|
1662
|
-
new_items = []
|
|
1663
|
-
sent = 0
|
|
1664
|
-
for item in new_items:
|
|
1665
|
-
tc = item["tc"]; result = item["result"]
|
|
1666
|
-
gidx = last_tool + sent
|
|
1667
|
-
tname = tc.get("name", "unknown")
|
|
1668
|
-
start_dt = _parse_ts_value(tc.get("_ts"))
|
|
1669
|
-
end_dt = _parse_ts_value(result.get("_ts")) if result else None
|
|
1670
|
-
# 每个 tool_call 用一个 model_call_+tool 子树(与 claude-code 视觉一致)
|
|
1671
|
-
mspan = client.start_span(name=f"model_call_{gidx}", span_type="model",
|
|
1672
|
-
start_time=start_dt, child_of=root_ctx)
|
|
1673
|
-
mspan.set_runtime(Runtime(library="codex-cli"))
|
|
1674
|
-
try: mspan.set_model_name(model_name)
|
|
1675
|
-
except Exception: pass
|
|
1676
|
-
if start_dt and end_dt:
|
|
1677
|
-
_set_finish_time_safe(mspan, end_dt)
|
|
1678
|
-
mspan.set_input(truncate_text(_json_text(tc.get("input", {}))))
|
|
1679
1837
|
mspan_ctx = client.get_span_from_header(mspan.to_header())
|
|
1680
1838
|
mspan.finish()
|
|
1681
1839
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1840
|
+
for tool_call in tool_calls:
|
|
1841
|
+
result = tool_call.get("result")
|
|
1842
|
+
if result is None:
|
|
1843
|
+
continue
|
|
1844
|
+
tool_start_dt = _parse_ts_value(tool_call.get("_ts")) or start_dt
|
|
1845
|
+
tool_end_dt = _parse_ts_value(result.get("_ts")) or tool_start_dt
|
|
1846
|
+
tname = tool_call.get("name", "unknown")
|
|
1847
|
+
tspan = client.start_span(
|
|
1848
|
+
name=f"tool_{tname}", span_type="tool",
|
|
1849
|
+
start_time=tool_start_dt, child_of=mspan_ctx,
|
|
1850
|
+
)
|
|
1851
|
+
tspan.set_runtime(Runtime(library="codex-cli"))
|
|
1852
|
+
if tool_end_dt is not None:
|
|
1853
|
+
_set_finish_time_safe(tspan, _max_dt(tool_start_dt, tool_end_dt))
|
|
1854
|
+
tspan.set_tags({
|
|
1855
|
+
"tool_name": tname,
|
|
1856
|
+
"tool_call_id": tool_call.get("call_id"),
|
|
1857
|
+
"model_invocation_line": line,
|
|
1858
|
+
})
|
|
1859
|
+
tspan.set_input(truncate_text(_json_text(tool_call.get("input", {}))))
|
|
1690
1860
|
tspan.set_output(truncate_text(_json_text(result.get("output", ""))))
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
client.flush() # 每个 tool_call 立即 flush → 结束即可见
|
|
1694
|
-
if upload_events:
|
|
1695
|
-
hook_log(f"[rt] upload failed after tool flush detail={upload_events[-1][:500]}")
|
|
1696
|
-
return None
|
|
1697
|
-
|
|
1698
|
-
state["rt_last_tool"] = last_tool + sent
|
|
1699
|
-
if sent > 0:
|
|
1700
|
-
state["rt_last_upload_ts"] = time.time()
|
|
1861
|
+
tspan.finish()
|
|
1862
|
+
tool_sent += 1
|
|
1701
1863
|
|
|
1702
|
-
# --- token_count 独立增量, 与 tool span 同一 root 下, 按行号水位线幂等 ---
|
|
1703
|
-
# 每条 token_count 挂一个 model_usage_<line> span:
|
|
1704
|
-
# last_token_usage → set_input_tokens/set_output_tokens (SDK 官方求和字段)
|
|
1705
|
-
# total_token_usage → tags (只观察, 不参与后端聚合, 否则会重复累加)
|
|
1706
|
-
# 只在 flush 成功后推进 rt_last_token_line, 保证上报失败下次能重发.
|
|
1707
|
-
token_sent = 0
|
|
1708
|
-
last_token_line = state.get("rt_last_token_line", -1)
|
|
1709
|
-
pending_tokens = [ev for ev in (token_events or []) if ev["line"] > last_token_line]
|
|
1710
|
-
max_flushed_line = last_token_line
|
|
1711
|
-
for ev in pending_tokens:
|
|
1712
|
-
line = ev["line"]
|
|
1713
|
-
last = ev.get("usage") or {}
|
|
1714
|
-
total = ev.get("total_usage") or {}
|
|
1715
|
-
in_tk = _usage_int(last, "input_tokens")
|
|
1716
|
-
out_tk = _usage_int(last, "output_tokens")
|
|
1717
|
-
start_dt = _parse_ts_value(ev.get("timestamp"))
|
|
1718
|
-
tk_span = client.start_span(name=f"model_usage_{line}", span_type="model",
|
|
1719
|
-
start_time=start_dt, child_of=root_ctx)
|
|
1720
|
-
tk_span.set_runtime(Runtime(library="codex-cli"))
|
|
1721
|
-
try: tk_span.set_model_name(model_name)
|
|
1722
|
-
except Exception: pass
|
|
1723
|
-
if start_dt is not None:
|
|
1724
|
-
_set_finish_time_safe(tk_span, start_dt)
|
|
1725
|
-
if in_tk > 0:
|
|
1726
|
-
try: tk_span.set_input_tokens(in_tk)
|
|
1727
|
-
except Exception: pass
|
|
1728
|
-
if out_tk > 0:
|
|
1729
|
-
try: tk_span.set_output_tokens(out_tk)
|
|
1730
|
-
except Exception: pass
|
|
1731
|
-
tk_tags = {
|
|
1732
|
-
"token_event_line": line,
|
|
1733
|
-
"token_source": "codex_token_count",
|
|
1734
|
-
"cached_input_tokens": _usage_int(last, "cached_input_tokens"),
|
|
1735
|
-
"reasoning_output_tokens": _usage_int(last, "reasoning_output_tokens"),
|
|
1736
|
-
"total_tokens": _usage_int(last, "total_tokens"),
|
|
1737
|
-
}
|
|
1738
|
-
if total:
|
|
1739
|
-
tk_tags["total_input_tokens"] = _usage_int(total, "input_tokens")
|
|
1740
|
-
tk_tags["total_output_tokens"] = _usage_int(total, "output_tokens")
|
|
1741
|
-
tk_tags["total_cached_input_tokens"] = _usage_int(total, "cached_input_tokens")
|
|
1742
|
-
tk_tags["total_reasoning_output_tokens"] = _usage_int(total, "reasoning_output_tokens")
|
|
1743
|
-
tk_tags["total_all_tokens"] = _usage_int(total, "total_tokens")
|
|
1744
|
-
tk_span.set_tags(tk_tags)
|
|
1745
|
-
tk_span.finish()
|
|
1746
1864
|
client.flush()
|
|
1747
1865
|
if upload_events:
|
|
1748
|
-
hook_log(
|
|
1749
|
-
|
|
1750
|
-
|
|
1866
|
+
hook_log(
|
|
1867
|
+
f"[rt] upload failed after invocation flush line={line} "
|
|
1868
|
+
f"detail={upload_events[-1][:500]}"
|
|
1869
|
+
)
|
|
1751
1870
|
return None
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1871
|
+
state["rt_last_invocation_line"] = line
|
|
1872
|
+
invocation_sent += 1
|
|
1873
|
+
|
|
1874
|
+
if invocation_sent > 0:
|
|
1875
|
+
state["rt_model_call_count"] = model_call_base + invocation_sent
|
|
1756
1876
|
state["rt_last_upload_ts"] = time.time()
|
|
1757
1877
|
|
|
1758
1878
|
if is_terminal:
|
|
@@ -1774,13 +1894,17 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1774
1894
|
if upload_events:
|
|
1775
1895
|
hook_log(f"[rt] upload failed after final flush detail={upload_events[-1][:500]}")
|
|
1776
1896
|
return None
|
|
1777
|
-
hook_log(f"[rt] finalized
|
|
1897
|
+
hook_log(f"[rt] finalized total_model_calls={state.get('rt_model_call_count', 0)}")
|
|
1778
1898
|
|
|
1779
1899
|
if upload_events:
|
|
1780
1900
|
hook_log(f"[rt] upload failed state not advanced detail={upload_events[-1][:500]}")
|
|
1781
1901
|
return None
|
|
1782
1902
|
|
|
1783
|
-
hook_log(
|
|
1903
|
+
hook_log(
|
|
1904
|
+
f"[rt] sent {invocation_sent} model invocation(s), {tool_sent} tool span(s), "
|
|
1905
|
+
f"last_invocation_line={state.get('rt_last_invocation_line', -1)}, "
|
|
1906
|
+
f"terminal={is_terminal}"
|
|
1907
|
+
)
|
|
1784
1908
|
return True
|
|
1785
1909
|
except Exception as e:
|
|
1786
1910
|
hook_log(f"[rt] error: {e}")
|
|
@@ -1855,8 +1979,7 @@ def main():
|
|
|
1855
1979
|
state_file = get_state_file_path(transcript_path)
|
|
1856
1980
|
state = load_state(state_file)
|
|
1857
1981
|
|
|
1858
|
-
#
|
|
1859
|
-
# state["rt_last_tool"] 增量,每次触发把新完成的 tool_call 发出,结束即可见,不节流。
|
|
1982
|
+
# 实时路径读取全量 entries 重建 invocation 序列,再用 token 边界行号水位线增量上报。
|
|
1860
1983
|
entries = read_rollout_messages(transcript_path, 0)
|
|
1861
1984
|
|
|
1862
1985
|
if not entries:
|
|
@@ -1898,9 +2021,13 @@ def main():
|
|
|
1898
2021
|
|
|
1899
2022
|
# Group entries into turns
|
|
1900
2023
|
turns = group_messages_into_turns(entries)
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2024
|
+
model_invocations = extract_model_invocations(
|
|
2025
|
+
turns, entries, is_terminal=is_terminal_event
|
|
2026
|
+
)
|
|
2027
|
+
debug_log(
|
|
2028
|
+
f"Grouped into {len(turns)} turns, "
|
|
2029
|
+
f"{len(model_invocations)} model invocations"
|
|
2030
|
+
)
|
|
1904
2031
|
|
|
1905
2032
|
# If this is a subagent, save data for parent to include later
|
|
1906
2033
|
if is_subagent:
|
|
@@ -1937,18 +2064,25 @@ def main():
|
|
|
1937
2064
|
break
|
|
1938
2065
|
coze_tags_override = state.get("coze_tags") or None
|
|
1939
2066
|
|
|
1940
|
-
# 方案 B
|
|
2067
|
+
# 方案 B 实时:按 token_count 边界把内容、推理、工具调用和 usage 合成 model_call。
|
|
1941
2068
|
ok = send_steps_realtime(turns, session_id, state, model_name=model_name,
|
|
1942
2069
|
coze_tags_override=coze_tags_override,
|
|
1943
2070
|
is_terminal=is_terminal_event,
|
|
1944
|
-
|
|
2071
|
+
model_invocations=model_invocations)
|
|
1945
2072
|
if ok is None:
|
|
1946
2073
|
hook_log(f"realtime send failed session_id={session_id}")
|
|
1947
2074
|
debug_log("Realtime send failed, state not advanced")
|
|
1948
2075
|
return
|
|
1949
2076
|
save_state(state_file, state)
|
|
1950
|
-
hook_log(
|
|
1951
|
-
|
|
2077
|
+
hook_log(
|
|
2078
|
+
f"rt state saved event={hook_event} "
|
|
2079
|
+
f"last_invocation_line={state.get('rt_last_invocation_line')} "
|
|
2080
|
+
f"session_id={session_id}"
|
|
2081
|
+
)
|
|
2082
|
+
debug_log(
|
|
2083
|
+
f"State saved. rt_last_invocation_line="
|
|
2084
|
+
f"{state.get('rt_last_invocation_line')}"
|
|
2085
|
+
)
|
|
1952
2086
|
else:
|
|
1953
2087
|
hook_log(f"skip no turns session_id={session_id}")
|
|
1954
2088
|
debug_log("No turns to send")
|
|
@@ -870,6 +870,34 @@ const cozeloopTracePlugin = {
|
|
|
870
870
|
parentSpanId: parentSpanId || ctx.rootSpanId,
|
|
871
871
|
};
|
|
872
872
|
};
|
|
873
|
+
const nativeTimingAttributes = (timing, fallbackSource = "reconstructed_session") => {
|
|
874
|
+
if (!timing)
|
|
875
|
+
return { timing_source: fallbackSource };
|
|
876
|
+
const attributes = {
|
|
877
|
+
timing_source: "native_model_call",
|
|
878
|
+
model_call_id: timing.callId,
|
|
879
|
+
model_call_outcome: timing.outcome,
|
|
880
|
+
};
|
|
881
|
+
if (timing.timeToFirstByteMs !== undefined)
|
|
882
|
+
attributes.time_to_first_byte_ms = timing.timeToFirstByteMs;
|
|
883
|
+
if (timing.requestPayloadBytes !== undefined)
|
|
884
|
+
attributes.request_payload_bytes = timing.requestPayloadBytes;
|
|
885
|
+
if (timing.responseStreamBytes !== undefined)
|
|
886
|
+
attributes.response_stream_bytes = timing.responseStreamBytes;
|
|
887
|
+
if (timing.errorCategory)
|
|
888
|
+
attributes.error_category = timing.errorCategory;
|
|
889
|
+
if (timing.failureKind)
|
|
890
|
+
attributes.failure_kind = timing.failureKind;
|
|
891
|
+
if (timing.outcome === "error")
|
|
892
|
+
attributes.error = true;
|
|
893
|
+
return attributes;
|
|
894
|
+
};
|
|
895
|
+
const consumeNativeModelTiming = (ctx) => {
|
|
896
|
+
const queue = ctx.completedNativeModelCalls;
|
|
897
|
+
if (!queue || queue.length === 0)
|
|
898
|
+
return undefined;
|
|
899
|
+
return queue.shift();
|
|
900
|
+
};
|
|
873
901
|
const buildReactSpans = async (ctx, channelId, entries, initialInput, agentStartTime, userWrittenAt, skipCount, sessionUserContent) => {
|
|
874
902
|
const entriesToSkip = skipCount || 0;
|
|
875
903
|
const reactMessages = [];
|
|
@@ -947,8 +975,9 @@ const cozeloopTracePlugin = {
|
|
|
947
975
|
modelSpanCount++;
|
|
948
976
|
const provider = entry.provider || ctx.lastModelProvider || "unknown";
|
|
949
977
|
const model = entry.model || ctx.lastModelId || "unknown";
|
|
950
|
-
const
|
|
951
|
-
const
|
|
978
|
+
const nativeTiming = consumeNativeModelTiming(ctx);
|
|
979
|
+
const spanStartTime = nativeTiming?.startTime ?? prevWrittenAt;
|
|
980
|
+
const spanEndTime = nativeTiming?.endTime ?? entryWrittenAt;
|
|
952
981
|
const tokenUsage = readTokenUsage(entry.usage);
|
|
953
982
|
const modelSpan = createSpan(ctx, channelId, `${provider}/${model}`, "model", spanStartTime, spanEndTime, {
|
|
954
983
|
"gen_ai.provider.name": provider,
|
|
@@ -959,6 +988,7 @@ const cozeloopTracePlugin = {
|
|
|
959
988
|
"input_tokens": tokenUsage.input,
|
|
960
989
|
"output_tokens": tokenUsage.output,
|
|
961
990
|
"react_round": reactRound,
|
|
991
|
+
...nativeTimingAttributes(nativeTiming),
|
|
962
992
|
}, { messages: reactMessages.map((msg) => safeClone(msg)) }, formatAssistantOutput(entry.content, entry.stopReason));
|
|
963
993
|
await exporter.export(modelSpan);
|
|
964
994
|
}
|
|
@@ -1161,9 +1191,47 @@ const cozeloopTracePlugin = {
|
|
|
1161
1191
|
}
|
|
1162
1192
|
});
|
|
1163
1193
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1194
|
+
if (shouldHookEnabled("model_call_started")) {
|
|
1195
|
+
on("model_call_started", async (event, hookCtx) => {
|
|
1196
|
+
const rawChannelId = resolveChannelId(hookCtx);
|
|
1197
|
+
const { ctx } = resolveActiveContext(rawChannelId, event.runId, "model_call_started", hookCtx);
|
|
1198
|
+
if (!ctx.nativeModelCallStarts)
|
|
1199
|
+
ctx.nativeModelCallStarts = new Map();
|
|
1200
|
+
ctx.nativeModelCallStarts.set(event.callId, {
|
|
1201
|
+
callId: event.callId,
|
|
1202
|
+
runId: event.runId,
|
|
1203
|
+
provider: event.provider,
|
|
1204
|
+
model: event.model,
|
|
1205
|
+
startTime: Date.now(),
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (shouldHookEnabled("model_call_ended")) {
|
|
1210
|
+
on("model_call_ended", async (event, hookCtx) => {
|
|
1211
|
+
const rawChannelId = resolveChannelId(hookCtx);
|
|
1212
|
+
const { ctx } = resolveActiveContext(rawChannelId, event.runId, "model_call_ended", hookCtx);
|
|
1213
|
+
const start = ctx.nativeModelCallStarts?.get(event.callId);
|
|
1214
|
+
if (!start)
|
|
1215
|
+
return;
|
|
1216
|
+
ctx.nativeModelCallStarts.delete(event.callId);
|
|
1217
|
+
const durationMs = Math.max(0, Number(event.durationMs) || 0);
|
|
1218
|
+
const endTime = Date.now();
|
|
1219
|
+
if (!ctx.completedNativeModelCalls)
|
|
1220
|
+
ctx.completedNativeModelCalls = [];
|
|
1221
|
+
ctx.completedNativeModelCalls.push({
|
|
1222
|
+
...start,
|
|
1223
|
+
startTime: endTime - durationMs,
|
|
1224
|
+
endTime,
|
|
1225
|
+
durationMs,
|
|
1226
|
+
outcome: event.outcome,
|
|
1227
|
+
errorCategory: event.errorCategory,
|
|
1228
|
+
failureKind: event.failureKind,
|
|
1229
|
+
requestPayloadBytes: event.requestPayloadBytes,
|
|
1230
|
+
responseStreamBytes: event.responseStreamBytes,
|
|
1231
|
+
timeToFirstByteMs: event.timeToFirstByteMs,
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1167
1235
|
if (shouldHookEnabled("llm_input")) {
|
|
1168
1236
|
on("llm_input", async (event, hookCtx) => {
|
|
1169
1237
|
const rawChannelId = resolveChannelId(hookCtx);
|
|
@@ -1248,9 +1316,6 @@ const cozeloopTracePlugin = {
|
|
|
1248
1316
|
ctx.llmInput = {
|
|
1249
1317
|
"messages": messages,
|
|
1250
1318
|
};
|
|
1251
|
-
lastLlmInput = ctx.llmInput;
|
|
1252
|
-
lastLlmStartTime = ctx.llmStartTime;
|
|
1253
|
-
lastLlmSpanId = ctx.llmSpanId;
|
|
1254
1319
|
if (config.debug) {
|
|
1255
1320
|
api.logger.info(`[CozeloopTrace] LLM input started: ${event.provider}/${event.model}, runId=${event.runId}, traceId=${ctx.traceId}`);
|
|
1256
1321
|
}
|
|
@@ -1267,7 +1332,7 @@ const cozeloopTracePlugin = {
|
|
|
1267
1332
|
}
|
|
1268
1333
|
const { ctx, channelId } = resolveActiveContext(rawChannelId, event.runId, "llm_output", hookCtx);
|
|
1269
1334
|
const now = Date.now();
|
|
1270
|
-
const startTime = ctx.llmStartTime ||
|
|
1335
|
+
const startTime = ctx.llmStartTime || now;
|
|
1271
1336
|
if (event.assistantTexts && event.assistantTexts.length > 0) {
|
|
1272
1337
|
const outputText = event.assistantTexts.join("\n");
|
|
1273
1338
|
ctx.lastOutput = outputText;
|
|
@@ -1279,8 +1344,8 @@ const cozeloopTracePlugin = {
|
|
|
1279
1344
|
api.logger.info(`[CozeloopTrace] Captured output from llm_output (will use last): traceId=${ctx.traceId}, length=${outputText.length}`);
|
|
1280
1345
|
}
|
|
1281
1346
|
}
|
|
1282
|
-
const llmInput = ctx.llmInput
|
|
1283
|
-
const llmSpanId = ctx.llmSpanId
|
|
1347
|
+
const llmInput = ctx.llmInput;
|
|
1348
|
+
const llmSpanId = ctx.llmSpanId;
|
|
1284
1349
|
if (config.debug) {
|
|
1285
1350
|
api.logger.info(`[CozeloopTrace] llm_output ctx: traceId=${ctx.traceId}, rootSpanId=${ctx.rootSpanId}, llmSpanId=${llmSpanId || "none"}, hasInput=${!!llmInput}`);
|
|
1286
1351
|
}
|
|
@@ -1289,7 +1354,7 @@ const cozeloopTracePlugin = {
|
|
|
1289
1354
|
const { entries, userWrittenAt, userContent } = readCurrentTurnReactSequence(hookCtx);
|
|
1290
1355
|
const hasAssistantEntry = entries.some((e) => e.type === "assistant");
|
|
1291
1356
|
if (entries.length > 0 && hasAssistantEntry) {
|
|
1292
|
-
const agentStart = ctx.agentStartTime || ctx.llmStartTime ||
|
|
1357
|
+
const agentStart = ctx.agentStartTime || ctx.llmStartTime || now;
|
|
1293
1358
|
const skipCount = ctx.sessionBasedExportedCount || 0;
|
|
1294
1359
|
const modelCount = await buildReactSpans(ctx, channelId, entries, llmInput, agentStart, userWrittenAt, skipCount, userContent);
|
|
1295
1360
|
if (modelCount > 0) {
|
|
@@ -1332,7 +1397,9 @@ const cozeloopTracePlugin = {
|
|
|
1332
1397
|
"output_tokens": tokenUsage.output,
|
|
1333
1398
|
};
|
|
1334
1399
|
const finalOutput = formatAssistantOutput(event.assistantTexts?.map((t) => ({ type: "text", text: t })) ?? [], "stop");
|
|
1335
|
-
const
|
|
1400
|
+
const nativeTiming = consumeNativeModelTiming(ctx);
|
|
1401
|
+
Object.assign(spanAttributes, nativeTimingAttributes(nativeTiming, "llm_hooks_fallback"));
|
|
1402
|
+
const span = createSpan(ctx, channelId, `${event.provider}/${event.model}`, "model", nativeTiming?.startTime ?? startTime, nativeTiming?.endTime ?? now, spanAttributes, llmInput, finalOutput);
|
|
1336
1403
|
if (llmSpanId) {
|
|
1337
1404
|
span.spanId = llmSpanId;
|
|
1338
1405
|
}
|
|
@@ -1350,9 +1417,6 @@ const cozeloopTracePlugin = {
|
|
|
1350
1417
|
ctx.reactCount = 0;
|
|
1351
1418
|
ctx.pendingToolSpans = undefined;
|
|
1352
1419
|
ctx.sessionBasedSpansCreated = undefined;
|
|
1353
|
-
lastLlmInput = undefined;
|
|
1354
|
-
lastLlmStartTime = undefined;
|
|
1355
|
-
lastLlmSpanId = undefined;
|
|
1356
1420
|
});
|
|
1357
1421
|
}
|
|
1358
1422
|
if (shouldHookEnabled("before_tool_call")) {
|