coze_lab 0.1.42 → 0.1.43

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -550,6 +550,19 @@ def read_new_messages(file_path: str, start_line: int = 0) -> List[Dict[str, Any
550
550
 
551
551
  # --- Content Helpers ---
552
552
 
553
+ def _usage_int(usage: Any, key: str) -> int:
554
+ """Read a token count from a usage dict, treating missing/None/非数字 一律为 0。
555
+
556
+ Claude Code transcript 里 usage 的 cache_* 等字段常【存在但值为 null】,dict.get(key, 0)
557
+ 对显式 null 返回 None 而非 0,后续 None + int / None > 0 会抛 TypeError,导致整个实时
558
+ 上报失败、trace 查不到。这里统一兜底。
559
+ """
560
+ if not isinstance(usage, dict):
561
+ return 0
562
+ v = usage.get(key)
563
+ return v if isinstance(v, int) else 0
564
+
565
+
553
566
  def is_empty_content(content: Any) -> bool:
554
567
  """Return True if content carries no meaningful data."""
555
568
  if content is None:
@@ -676,7 +689,7 @@ def _group_subagent_steps(progress_msgs: List[Dict[str, Any]]) -> List[Dict[str,
676
689
  existing.extend(content)
677
690
  last_step["tool_calls"].extend(tool_calls)
678
691
  usage = pmsg.get("usage", {})
679
- if usage.get("input_tokens", 0) > 0 or usage.get("output_tokens", 0) > 0:
692
+ if _usage_int(usage, "input_tokens") > 0 or _usage_int(usage, "output_tokens") > 0:
680
693
  last_step["assistant_message"]["message"]["usage"] = usage
681
694
  else:
682
695
  steps.append({
@@ -849,7 +862,7 @@ def group_messages_into_turns(messages: List[Dict[str, Any]]) -> List[Dict[str,
849
862
  last_step["tool_calls"].extend(tool_calls)
850
863
  # Carry over usage from the later line (earlier line typically has zeros)
851
864
  usage = message.get("usage", {})
852
- if usage.get("input_tokens", 0) > 0 or usage.get("output_tokens", 0) > 0:
865
+ if _usage_int(usage, "input_tokens") > 0 or _usage_int(usage, "output_tokens") > 0:
853
866
  last_step["assistant_message"]["message"]["usage"] = usage
854
867
  else:
855
868
  # New API response — create a new step
@@ -1329,10 +1342,10 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1329
1342
 
1330
1343
  # Set token usage for this specific model call
1331
1344
  usage = assistant_message_obj.get("usage", {})
1332
- input_tokens = usage.get("input_tokens", 0)
1333
- output_tokens = usage.get("output_tokens", 0)
1334
- cache_creation = usage.get("cache_creation_input_tokens", 0)
1335
- cache_read = usage.get("cache_read_input_tokens", 0)
1345
+ input_tokens = _usage_int(usage, "input_tokens")
1346
+ output_tokens = _usage_int(usage, "output_tokens")
1347
+ cache_creation = _usage_int(usage, "cache_creation_input_tokens")
1348
+ cache_read = _usage_int(usage, "cache_read_input_tokens")
1336
1349
  if input_tokens > 0 or cache_creation > 0 or cache_read > 0:
1337
1350
  model_span.set_input_tokens(input_tokens + cache_creation + cache_read)
1338
1351
  if output_tokens > 0:
@@ -1402,10 +1415,10 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1402
1415
 
1403
1416
  # Distribute total usage evenly across sub-agent model steps.
1404
1417
  total_usage = tool_call.get("_total_usage", {})
1405
- total_in = (total_usage.get("input_tokens", 0)
1406
- + total_usage.get("cache_creation_input_tokens", 0)
1407
- + total_usage.get("cache_read_input_tokens", 0))
1408
- total_out = total_usage.get("output_tokens", 0)
1418
+ total_in = (_usage_int(total_usage, "input_tokens")
1419
+ + _usage_int(total_usage, "cache_creation_input_tokens")
1420
+ + _usage_int(total_usage, "cache_read_input_tokens"))
1421
+ total_out = _usage_int(total_usage, "output_tokens")
1409
1422
  n_model_steps = len(sub_steps)
1410
1423
  per_step_in = total_in // n_model_steps if n_model_steps > 0 else 0
1411
1424
  per_step_out = total_out // n_model_steps if n_model_steps > 0 else 0
@@ -1778,12 +1791,12 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
1778
1791
  if text_parts:
1779
1792
  mspan.set_output("\n".join(text_parts))
1780
1793
  usage = amo.get("usage", {})
1781
- it_tok = usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + usage.get("cache_read_input_tokens", 0)
1794
+ it_tok = _usage_int(usage, "input_tokens") + _usage_int(usage, "cache_creation_input_tokens") + _usage_int(usage, "cache_read_input_tokens")
1782
1795
  if it_tok > 0:
1783
1796
  try: mspan.set_input_tokens(it_tok)
1784
1797
  except Exception: pass
1785
- if usage.get("output_tokens", 0) > 0:
1786
- try: mspan.set_output_tokens(usage["output_tokens"])
1798
+ if _usage_int(usage, "output_tokens") > 0:
1799
+ try: mspan.set_output_tokens(_usage_int(usage, "output_tokens"))
1787
1800
  except Exception: pass
1788
1801
  mspan_ctx = client.get_span_from_header(mspan.to_header())
1789
1802
  mspan.finish()
@@ -787,6 +787,18 @@ def truncate_text(text: str, limit: int = 12000) -> str:
787
787
 
788
788
  # --- Message Grouping ---
789
789
 
790
+ def _usage_int(usage, key):
791
+ """从 token_usage dict 读 token 数,missing/None/非数字 一律按 0。
792
+
793
+ transcript 里 token_usage 字段可能【存在但值为 null】,dict.get(key, 0) 对显式 null
794
+ 返回 None 而非 0,后续 None > 0 / None + int 会抛 TypeError 中断上报。这里统一兜底。
795
+ """
796
+ if not isinstance(usage, dict):
797
+ return 0
798
+ v = usage.get(key)
799
+ return v if isinstance(v, int) else 0
800
+
801
+
790
802
  def _parse_ts(obj):
791
803
  """从 codex entry/payload 的 timestamp 解析 datetime(带时区)。失败返回 None。
792
804
 
@@ -1299,8 +1311,8 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
1299
1311
 
1300
1312
  # Set token usage
1301
1313
  token_usage = turn.get("token_usage", {})
1302
- input_tokens = token_usage.get("input_tokens", 0)
1303
- output_tokens = token_usage.get("output_tokens", 0)
1314
+ input_tokens = _usage_int(token_usage, "input_tokens")
1315
+ output_tokens = _usage_int(token_usage, "output_tokens")
1304
1316
  if input_tokens > 0:
1305
1317
  model_span.set_input_tokens(input_tokens)
1306
1318
  if output_tokens > 0:
@@ -1446,10 +1458,12 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
1446
1458
  sa_model_span.set_output(ModelOutput(choices=sa_choices))
1447
1459
 
1448
1460
  sa_token = sa_turn.get("token_usage", {})
1449
- if sa_token.get("input_tokens", 0) > 0:
1450
- sa_model_span.set_input_tokens(sa_token["input_tokens"])
1451
- if sa_token.get("output_tokens", 0) > 0:
1452
- sa_model_span.set_output_tokens(sa_token["output_tokens"])
1461
+ sa_in = _usage_int(sa_token, "input_tokens")
1462
+ sa_out = _usage_int(sa_token, "output_tokens")
1463
+ if sa_in > 0:
1464
+ sa_model_span.set_input_tokens(sa_in)
1465
+ if sa_out > 0:
1466
+ sa_model_span.set_output_tokens(sa_out)
1453
1467
 
1454
1468
  # Subagent tool spans
1455
1469
  for sa_tc in sa_turn.get("tool_calls", []):