octarin-cli 0.3.2 → 0.3.4
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/assets/backfill.py
CHANGED
|
@@ -290,6 +290,14 @@ def parse_claude_transcript( # noqa: PLR0915 - top-down jsonl parser; splitting
|
|
|
290
290
|
:_OUTPUT_CAP
|
|
291
291
|
],
|
|
292
292
|
"status": "ok",
|
|
293
|
+
# subagent turns bill against the session model;
|
|
294
|
+
# carry it so per-model analytics can attribute
|
|
295
|
+
# their tokens
|
|
296
|
+
**(
|
|
297
|
+
{"model": model}
|
|
298
|
+
if stype == "agent" and model
|
|
299
|
+
else {}
|
|
300
|
+
),
|
|
293
301
|
}
|
|
294
302
|
)
|
|
295
303
|
prev_ts = ts
|
|
@@ -347,6 +347,11 @@ def build_spans(entries: list[dict]) -> tuple[list[dict], dict, list[str], str |
|
|
|
347
347
|
|
|
348
348
|
pending_user_text = ""
|
|
349
349
|
pending_user_attachments: list[dict] = []
|
|
350
|
+
# One API generation streams as SEVERAL transcript entries (one per content
|
|
351
|
+
# block — text, then each tool_use) that share the same message id and each
|
|
352
|
+
# repeat the generation's FULL usage. Merge them into ONE span keyed by that
|
|
353
|
+
# id: usage/cost counted once, outputs concatenated, tool children attached.
|
|
354
|
+
llm_span_by_id: dict[str, dict] = {}
|
|
350
355
|
# ts of the previous transcript entry; the LLM call started when the user
|
|
351
356
|
# prompt / tool result landed, finished when the assistant message appears.
|
|
352
357
|
prev_ts: str | None = None
|
|
@@ -373,6 +378,31 @@ def build_spans(entries: list[dict]) -> tuple[list[dict], dict, list[str], str |
|
|
|
373
378
|
span_id = _msg(entry).get("id") or uuid.uuid4().hex
|
|
374
379
|
out_text = _truncate(_text(content))
|
|
375
380
|
|
|
381
|
+
existing = llm_span_by_id.get(str(span_id))
|
|
382
|
+
if existing is not None:
|
|
383
|
+
# Continuation entry of an already-seen generation: extend the span,
|
|
384
|
+
# never re-count its usage (each entry repeats the full totals).
|
|
385
|
+
existing["end_time"] = ts
|
|
386
|
+
if out_text:
|
|
387
|
+
joined = (
|
|
388
|
+
f"{existing['output']}\n{out_text}" if existing["output"] else out_text
|
|
389
|
+
)
|
|
390
|
+
existing["output"] = _truncate(joined)
|
|
391
|
+
for tu in _blocks(content, "tool_use"):
|
|
392
|
+
_append_tool_span(
|
|
393
|
+
spans,
|
|
394
|
+
totals,
|
|
395
|
+
tu,
|
|
396
|
+
parent_span_id=str(span_id),
|
|
397
|
+
ts=ts,
|
|
398
|
+
results_by_id=results_by_id,
|
|
399
|
+
errors_by_id=errors_by_id,
|
|
400
|
+
result_ts_by_id=result_ts_by_id,
|
|
401
|
+
attachments_by_tool_id=attachments_by_tool_id,
|
|
402
|
+
)
|
|
403
|
+
prev_ts = ts
|
|
404
|
+
continue
|
|
405
|
+
|
|
376
406
|
in_tok = usage.get("input", 0)
|
|
377
407
|
out_tok = usage.get("output", 0)
|
|
378
408
|
cache_r = usage.get("cache_read", 0)
|
|
@@ -399,6 +429,7 @@ def build_spans(entries: list[dict]) -> tuple[list[dict], dict, list[str], str |
|
|
|
399
429
|
if pending_user_attachments:
|
|
400
430
|
llm_span["attachments"] = pending_user_attachments
|
|
401
431
|
spans.append(llm_span)
|
|
432
|
+
llm_span_by_id[str(span_id)] = llm_span
|
|
402
433
|
pending_user_text = "" # consumed by this generation
|
|
403
434
|
pending_user_attachments = [] # consumed by this generation
|
|
404
435
|
|
|
@@ -408,29 +439,17 @@ def build_spans(entries: list[dict]) -> tuple[list[dict], dict, list[str], str |
|
|
|
408
439
|
totals["total_tokens"] += in_tok + out_tok
|
|
409
440
|
|
|
410
441
|
for tu in _blocks(content, "tool_use"):
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
442
|
+
_append_tool_span(
|
|
443
|
+
spans,
|
|
444
|
+
totals,
|
|
445
|
+
tu,
|
|
446
|
+
parent_span_id=str(span_id),
|
|
447
|
+
ts=ts,
|
|
448
|
+
results_by_id=results_by_id,
|
|
449
|
+
errors_by_id=errors_by_id,
|
|
450
|
+
result_ts_by_id=result_ts_by_id,
|
|
451
|
+
attachments_by_tool_id=attachments_by_tool_id,
|
|
416
452
|
)
|
|
417
|
-
tool_span = {
|
|
418
|
-
"span_id": tid,
|
|
419
|
-
"parent_span_id": str(span_id),
|
|
420
|
-
"name": f"Tool: {tname}",
|
|
421
|
-
"span_type": "tool",
|
|
422
|
-
"start_time": ts,
|
|
423
|
-
"end_time": result_ts_by_id.get(tid, ts),
|
|
424
|
-
"input": _truncate(input_str),
|
|
425
|
-
"output": _truncate(results_by_id.get(tid, "")) or None,
|
|
426
|
-
"status": "error" if errors_by_id.get(tid) else "ok",
|
|
427
|
-
"attributes": {"tool_name": tname, "tool_id": tid},
|
|
428
|
-
}
|
|
429
|
-
tool_atts = attachments_by_tool_id.get(tid)
|
|
430
|
-
if tool_atts:
|
|
431
|
-
tool_span["attachments"] = tool_atts
|
|
432
|
-
spans.append(tool_span)
|
|
433
|
-
totals["tool_call_count"] += 1
|
|
434
453
|
|
|
435
454
|
prev_ts = ts
|
|
436
455
|
|
|
@@ -438,6 +457,42 @@ def build_spans(entries: list[dict]) -> tuple[list[dict], dict, list[str], str |
|
|
|
438
457
|
return spans, totals, models, None
|
|
439
458
|
|
|
440
459
|
|
|
460
|
+
def _append_tool_span(
|
|
461
|
+
spans: list[dict],
|
|
462
|
+
totals: dict,
|
|
463
|
+
tu: dict,
|
|
464
|
+
*,
|
|
465
|
+
parent_span_id: str,
|
|
466
|
+
ts: str,
|
|
467
|
+
results_by_id: dict[str, str],
|
|
468
|
+
errors_by_id: dict[str, bool],
|
|
469
|
+
result_ts_by_id: dict[str, str],
|
|
470
|
+
attachments_by_tool_id: dict[str, list[dict]],
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Append one ``tool`` child span for a ``tool_use`` block to ``spans``."""
|
|
473
|
+
tid = str(tu.get("id") or uuid.uuid4().hex)
|
|
474
|
+
tname = tu.get("name") or "unknown"
|
|
475
|
+
tu_input = tu.get("input")
|
|
476
|
+
input_str = tu_input if isinstance(tu_input, str) else json.dumps(tu_input, ensure_ascii=False)
|
|
477
|
+
tool_span = {
|
|
478
|
+
"span_id": tid,
|
|
479
|
+
"parent_span_id": parent_span_id,
|
|
480
|
+
"name": f"Tool: {tname}",
|
|
481
|
+
"span_type": "tool",
|
|
482
|
+
"start_time": ts,
|
|
483
|
+
"end_time": result_ts_by_id.get(tid, ts),
|
|
484
|
+
"input": _truncate(input_str),
|
|
485
|
+
"output": _truncate(results_by_id.get(tid, "")) or None,
|
|
486
|
+
"status": "error" if errors_by_id.get(tid) else "ok",
|
|
487
|
+
"attributes": {"tool_name": tname, "tool_id": tid},
|
|
488
|
+
}
|
|
489
|
+
tool_atts = attachments_by_tool_id.get(tid)
|
|
490
|
+
if tool_atts:
|
|
491
|
+
tool_span["attachments"] = tool_atts
|
|
492
|
+
spans.append(tool_span)
|
|
493
|
+
totals["tool_call_count"] += 1
|
|
494
|
+
|
|
495
|
+
|
|
441
496
|
def user_ref() -> str:
|
|
442
497
|
"""Resolve the engineer's real identity for attribution.
|
|
443
498
|
|
|
@@ -128,6 +128,9 @@ function toolSpanId(input) {
|
|
|
128
128
|
function fromPostToolUse(input) {
|
|
129
129
|
const tool = input.tool_name || "tool";
|
|
130
130
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
131
|
+
// Cursor never exposes token usage, so the backend estimates tool spans from
|
|
132
|
+
// their I/O — stamp the session model so the estimate prices at its real rate.
|
|
133
|
+
span.model = input.model || null;
|
|
131
134
|
span.input = truncate(asText(input.tool_input));
|
|
132
135
|
span.output = truncate(asText(input.tool_output));
|
|
133
136
|
span.attributes = {
|
|
@@ -144,6 +147,7 @@ function fromPostToolUse(input) {
|
|
|
144
147
|
function fromPostToolUseFailure(input) {
|
|
145
148
|
const tool = input.tool_name || "tool";
|
|
146
149
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
150
|
+
span.model = input.model || null;
|
|
147
151
|
span.status = "error";
|
|
148
152
|
span.error_message = input.error_message || input.failure_type || "tool failed";
|
|
149
153
|
span.input = truncate(asText(input.tool_input));
|
|
@@ -128,6 +128,9 @@ function toolSpanId(input) {
|
|
|
128
128
|
function fromPostToolUse(input) {
|
|
129
129
|
const tool = input.tool_name || "tool";
|
|
130
130
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
131
|
+
// Cursor never exposes token usage, so the backend estimates tool spans from
|
|
132
|
+
// their I/O — stamp the session model so the estimate prices at its real rate.
|
|
133
|
+
span.model = input.model || null;
|
|
131
134
|
span.input = truncate(asText(input.tool_input));
|
|
132
135
|
span.output = truncate(asText(input.tool_output));
|
|
133
136
|
span.attributes = {
|
|
@@ -144,6 +147,7 @@ function fromPostToolUse(input) {
|
|
|
144
147
|
function fromPostToolUseFailure(input) {
|
|
145
148
|
const tool = input.tool_name || "tool";
|
|
146
149
|
const span = baseSpan(toolSpanId(input), tool, "tool");
|
|
150
|
+
span.model = input.model || null;
|
|
147
151
|
span.status = "error";
|
|
148
152
|
span.error_message = input.error_message || input.failure_type || "tool failed";
|
|
149
153
|
span.input = truncate(asText(input.tool_input));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octarin-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Octarin's per-user CLI: install AI-coding capture (`octarin init` / `init-repo`) and authorize a machine (`octarin login`). Streams your Claude Code / Cursor / Codex usage to your Octarin workspace.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"octarin",
|