coze_lab 0.1.55 → 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)
|
|
@@ -822,6 +822,12 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
|
|
|
822
822
|
}
|
|
823
823
|
seen = {"content": set(), "reasoning": set()}
|
|
824
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
|
+
|
|
825
831
|
def append_text(category: str, value: Any):
|
|
826
832
|
if not isinstance(value, str):
|
|
827
833
|
return
|
|
@@ -842,7 +848,11 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
|
|
|
842
848
|
if not has_payload:
|
|
843
849
|
return
|
|
844
850
|
current["line"] = boundary_line
|
|
845
|
-
current["end_timestamp"]
|
|
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"
|
|
846
856
|
current["usage"] = usage or {}
|
|
847
857
|
current["total_usage"] = total_usage or {}
|
|
848
858
|
current["content"] = "\n".join(current.pop("content_parts"))
|
|
@@ -866,7 +876,8 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
|
|
|
866
876
|
"protected": {"source": "", "count": 0, "bytes": 0},
|
|
867
877
|
"tool_calls": [],
|
|
868
878
|
"turn": None,
|
|
869
|
-
"start_timestamp":
|
|
879
|
+
"start_timestamp": event.get("timestamp"),
|
|
880
|
+
"start_turn": event.get("turn"),
|
|
870
881
|
"end_timestamp": None,
|
|
871
882
|
"line": -1,
|
|
872
883
|
"usage": {},
|
|
@@ -876,7 +887,14 @@ def extract_model_invocations(turns: List[Dict[str, Any]],
|
|
|
876
887
|
continue
|
|
877
888
|
|
|
878
889
|
current["turn"] = current["turn"] or event.get("turn")
|
|
879
|
-
current
|
|
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
|
+
)
|
|
880
898
|
current["end_timestamp"] = event.get("timestamp") or current["end_timestamp"]
|
|
881
899
|
current["line"] = max(current["line"], event.get("line", -1))
|
|
882
900
|
if kind == "record":
|
|
@@ -944,6 +962,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
944
962
|
"subagent_calls": [],
|
|
945
963
|
"token_usage": {},
|
|
946
964
|
"start_line": entry.get("_line_number", 0),
|
|
965
|
+
"start_timestamp": payload.get("_ts"),
|
|
947
966
|
}
|
|
948
967
|
pending_calls = {}
|
|
949
968
|
elif msg_type == "task_complete":
|
|
@@ -1793,6 +1812,7 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1793
1812
|
tags = {
|
|
1794
1813
|
"invocation_boundary_line": line,
|
|
1795
1814
|
"token_source": "codex_token_count" if usage else "unavailable",
|
|
1815
|
+
"timing_source": invocation.get("timing_source", "reconstructed"),
|
|
1796
1816
|
"cached_input_tokens": _usage_int(usage, "cached_input_tokens"),
|
|
1797
1817
|
"reasoning_output_tokens": _usage_int(usage, "reasoning_output_tokens"),
|
|
1798
1818
|
"total_tokens": _usage_int(usage, "total_tokens"),
|
|
@@ -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")) {
|