coze_lab 0.1.49 → 0.1.51

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
@@ -669,14 +669,12 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
669
669
  delete existing.env.COZELAB_ONBOARD_CLOUD;
670
670
  }
671
671
  const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
672
- const otelEndpoint = readEnv('OTEL_ENDPOINT');
673
672
  if (loopBaseUrl) {
674
673
  existing.env.COZELOOP_API_BASE_URL = loopBaseUrl;
675
- delete existing.env.OTEL_ENDPOINT;
676
- } else if (otelEndpoint) {
677
- existing.env.OTEL_ENDPOINT = otelEndpoint;
674
+ } else {
678
675
  delete existing.env.COZELOOP_API_BASE_URL;
679
676
  }
677
+ delete existing.env.OTEL_ENDPOINT;
680
678
  // 切正式环境:移除历史注入的 PPE 泳道 env(旧版本曾写入 x_tt_env/x_use_ppe)
681
679
  delete existing.env.x_tt_env;
682
680
  delete existing.env.x_use_ppe;
@@ -733,11 +731,8 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
733
731
  envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
734
732
  envLines.push('TRACE_TO_COZELOOP=true');
735
733
  const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
736
- const otelEndpoint = readEnv('OTEL_ENDPOINT');
737
734
  if (loopBaseUrl) {
738
735
  envLines.push(shellEnvLine('COZELOOP_API_BASE_URL', loopBaseUrl));
739
- } else if (otelEndpoint) {
740
- envLines.push(shellEnvLine('OTEL_ENDPOINT', otelEndpoint));
741
736
  }
742
737
  const envContent = envLines.join('\n') + '\n';
743
738
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -108,7 +108,7 @@ _DEFAULT_WORKSPACE_ID = "7649231955045072915" # hardcoded spaceID fallback
108
108
 
109
109
 
110
110
  def _normalize_api_base_url(raw: str) -> str:
111
- """Normalize CozeLoop API/OTLP endpoint env vars to the SDK api_base_url."""
111
+ """Normalize a CozeLoop API/ingest URL to the SDK api_base_url."""
112
112
  base = (raw or "").strip().rstrip("/")
113
113
  if not base:
114
114
  return ""
@@ -130,11 +130,7 @@ def _normalize_api_base_url(raw: str) -> str:
130
130
 
131
131
  def get_api_base_url() -> str:
132
132
  """Return optional CozeLoop SDK api_base_url from onboard-injected env."""
133
- return _normalize_api_base_url(
134
- os.environ.get("COZELOOP_API_BASE_URL")
135
- or os.environ.get("OTEL_ENDPOINT")
136
- or ""
137
- )
133
+ return _normalize_api_base_url(os.environ.get("COZELOOP_API_BASE_URL") or "")
138
134
 
139
135
 
140
136
  # --- coze-context parsing -------------------------------------------------
@@ -255,7 +255,7 @@ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
255
255
 
256
256
 
257
257
  def _normalize_api_base_url(raw: str) -> str:
258
- """Normalize CozeLoop API/OTLP endpoint env vars to the SDK api_base_url."""
258
+ """Normalize a CozeLoop API/ingest URL to the SDK api_base_url."""
259
259
  base = (raw or "").strip().rstrip("/")
260
260
  if not base:
261
261
  return ""
@@ -277,11 +277,7 @@ def _normalize_api_base_url(raw: str) -> str:
277
277
 
278
278
  def get_api_base_url() -> str:
279
279
  """Return optional CozeLoop SDK api_base_url from onboard-injected env."""
280
- return _normalize_api_base_url(
281
- os.environ.get("COZELOOP_API_BASE_URL")
282
- or os.environ.get("OTEL_ENDPOINT")
283
- or ""
284
- )
280
+ return _normalize_api_base_url(os.environ.get("COZELOOP_API_BASE_URL") or "")
285
281
 
286
282
 
287
283
  def _log_file_path() -> str:
@@ -699,6 +695,32 @@ def _assistant_bounds(turn):
699
695
  return min(values), max(values)
700
696
 
701
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
+
702
724
  def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
703
725
  """Group raw JSONL entries into conversation turns.
704
726
 
@@ -1365,7 +1387,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
1365
1387
  # tool span 挂到固定 root、当场 flush。codex 的实时单元是 tool_call(turn 内平铺,按 call_id 关联
1366
1388
  # result)。全局水位线 state["rt_last_tool"] 跟踪已发的 tool_call 数。
1367
1389
 
1368
- 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):
1369
1391
  import cozeloop
1370
1392
  from cozeloop.spec.tracespec import Runtime
1371
1393
  token = get_fresh_token()
@@ -1498,6 +1520,62 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1498
1520
  if sent > 0:
1499
1521
  state["rt_last_upload_ts"] = time.time()
1500
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
+
1501
1579
  if is_terminal:
1502
1580
  # 收尾:补最终 assistant 输出
1503
1581
  last_out = ""
@@ -1523,7 +1601,7 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
1523
1601
  hook_log(f"[rt] upload failed state not advanced detail={upload_events[-1][:500]}")
1524
1602
  return None
1525
1603
 
1526
- 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}")
1527
1605
  return True
1528
1606
  except Exception as e:
1529
1607
  hook_log(f"[rt] error: {e}")
@@ -1641,7 +1719,9 @@ def main():
1641
1719
 
1642
1720
  # Group entries into turns
1643
1721
  turns = group_messages_into_turns(entries)
1644
- debug_log(f"Grouped into {len(turns)} turns")
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")
1645
1725
 
1646
1726
  # If this is a subagent, save data for parent to include later
1647
1727
  if is_subagent:
@@ -1681,7 +1761,8 @@ def main():
1681
1761
  # 方案 B 实时:把新完成的 tool_call 作为 span 挂稳定 root,结束即可见。不节流。
1682
1762
  ok = send_steps_realtime(turns, session_id, state, model_name=model_name,
1683
1763
  coze_tags_override=coze_tags_override,
1684
- is_terminal=is_terminal_event)
1764
+ is_terminal=is_terminal_event,
1765
+ token_events=token_events)
1685
1766
  if ok is None:
1686
1767
  hook_log(f"realtime send failed session_id={session_id}")
1687
1768
  debug_log("Realtime send failed, state not advanced")