@ictechgy/context-guard 0.4.15 → 0.4.16
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/CHANGELOG.md +8 -0
- package/README.ko.md +46 -1
- package/README.md +58 -2
- package/package.json +1 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +18 -0
- package/plugins/context-guard/README.md +18 -0
- package/plugins/context-guard/bin/context-guard-artifact +90 -9
- package/plugins/context-guard/bin/context-guard-audit +169 -66
- package/plugins/context-guard/bin/context-guard-bench +5765 -224
- package/plugins/context-guard/bin/context-guard-compress +90 -8
- package/plugins/context-guard/bin/context-guard-diet +1 -7
- package/plugins/context-guard/bin/context-guard-experiments +5 -1
- package/plugins/context-guard/bin/context-guard-failed-nudge +705 -83
- package/plugins/context-guard/bin/context-guard-guard-read +490 -55
- package/plugins/context-guard/bin/context-guard-pack +110 -11
- package/plugins/context-guard/bin/context-guard-read-symbol +7 -2
- package/plugins/context-guard/bin/context-guard-rewrite-bash +2204 -223
- package/plugins/context-guard/bin/context-guard-sanitize-output +560 -85
- package/plugins/context-guard/bin/context-guard-setup +1073 -147
- package/plugins/context-guard/bin/context-guard-statusline +131 -54
- package/plugins/context-guard/bin/context-guard-statusline-merged +7 -3
- package/plugins/context-guard/bin/context-guard-tool-prune +44 -11
- package/plugins/context-guard/bin/context-guard-trim-output +89 -13
- package/plugins/context-guard/brief/README.md +19 -0
- package/plugins/context-guard/brief/narration-mode.quiet.md +21 -0
- package/plugins/context-guard/lib/context_guard_commands.py +6 -2
- package/plugins/context-guard/lib/credential_policy.py +177 -0
- package/plugins/context-guard/lib/transcript_usage_reducer.py +378 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Best-effort Claude Code transcript usage auditor.
|
|
3
3
|
|
|
4
|
-
Claude Code transcript schemas may change.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Claude Code transcript schemas may change. Token totals use the deterministic
|
|
5
|
+
``row.message.usage`` contract; other bounded usage-like shapes mark results
|
|
6
|
+
partial instead of being silently counted. Cost and diagnostic metadata retain
|
|
7
|
+
their bounded schema-tolerant scans. Parse/read skips are reported so totals are
|
|
8
|
+
not mistaken for billing-authoritative data.
|
|
7
9
|
"""
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
@@ -23,6 +25,19 @@ from dataclasses import dataclass, field
|
|
|
23
25
|
from pathlib import Path
|
|
24
26
|
from typing import Any, BinaryIO, Iterable
|
|
25
27
|
|
|
28
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
29
|
+
_REDUCER_DIR = _SCRIPT_DIR
|
|
30
|
+
if not (_REDUCER_DIR / "transcript_usage_reducer.py").is_file():
|
|
31
|
+
_REDUCER_DIR = _SCRIPT_DIR.parent / "lib"
|
|
32
|
+
if str(_REDUCER_DIR) not in sys.path:
|
|
33
|
+
sys.path.insert(0, str(_REDUCER_DIR))
|
|
34
|
+
|
|
35
|
+
from transcript_usage_reducer import ( # noqa: E402
|
|
36
|
+
REDUCER_SCHEMA,
|
|
37
|
+
UsageReducer,
|
|
38
|
+
hash_file_identity,
|
|
39
|
+
)
|
|
40
|
+
|
|
26
41
|
TOKEN_KEY_GROUPS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
27
42
|
("input", ("input_tokens",)),
|
|
28
43
|
("output", ("output_tokens",)),
|
|
@@ -188,6 +203,9 @@ class UsageSummary:
|
|
|
188
203
|
cache_record_timestamps: list[_dt.datetime] = field(default_factory=list)
|
|
189
204
|
positive_cache_record_timestamps: list[_dt.datetime] = field(default_factory=list)
|
|
190
205
|
prompt_cache_audit: PromptCacheAudit = field(default_factory=PromptCacheAudit)
|
|
206
|
+
usage_reducer_schema: str = REDUCER_SCHEMA
|
|
207
|
+
usage_reducer_counters: Counter[str] = field(default_factory=Counter)
|
|
208
|
+
usage_reducer_partial: bool = False
|
|
191
209
|
cache_friendliness_cache: dict[str, Any] | None = field(default=None, init=False, repr=False)
|
|
192
210
|
cache_diagnostics_cache: dict[str, Any] | None = field(default=None, init=False, repr=False)
|
|
193
211
|
cache_layout_advice_cache: dict[str, Any] | None = field(default=None, init=False, repr=False)
|
|
@@ -242,11 +260,7 @@ def iter_jsonl_files(paths: Iterable[str]) -> Iterable[Path]:
|
|
|
242
260
|
if path.is_file() and path.suffix in {".jsonl", ".json"}:
|
|
243
261
|
candidates = [path]
|
|
244
262
|
elif path.is_dir():
|
|
245
|
-
candidates = (
|
|
246
|
-
candidate
|
|
247
|
-
for pattern in ("*.jsonl", "*.json")
|
|
248
|
-
for candidate in path.rglob(pattern)
|
|
249
|
-
)
|
|
263
|
+
candidates = sorted(path.rglob("*.jsonl"))
|
|
250
264
|
else:
|
|
251
265
|
continue
|
|
252
266
|
for candidate in candidates:
|
|
@@ -720,53 +734,9 @@ def add_usage(
|
|
|
720
734
|
show_paths: bool = False,
|
|
721
735
|
show_commands: bool = False,
|
|
722
736
|
) -> RecordUsage:
|
|
723
|
-
root_model = None
|
|
724
|
-
root_query_source = None
|
|
725
|
-
parsed_timestamp = None
|
|
726
|
-
if isinstance(root, dict):
|
|
727
|
-
root_model = first_string(root, MODEL_KEYS)
|
|
728
|
-
root_query_source = first_string(root, QUERY_SOURCE_KEYS)
|
|
729
|
-
parsed_timestamp = record_timestamp(root)
|
|
730
|
-
|
|
731
737
|
record = RecordUsage()
|
|
732
|
-
cache_telemetry_present = False
|
|
733
|
-
positive_cache_telemetry_present = False
|
|
734
738
|
summary.prompt_cache_audit.observe(root)
|
|
735
739
|
for d in walk(root):
|
|
736
|
-
local_tokens: Counter[str] = Counter()
|
|
737
|
-
present_buckets = add_token_groups(local_tokens, d)
|
|
738
|
-
|
|
739
|
-
# OpenTelemetry-style records sometimes use {name, value, attributes.type}.
|
|
740
|
-
name = d.get("name") or d.get("metric")
|
|
741
|
-
if name == "claude_code.token.usage":
|
|
742
|
-
value = d.get("value")
|
|
743
|
-
if value is None:
|
|
744
|
-
value = d.get("sum")
|
|
745
|
-
if value is None:
|
|
746
|
-
value = d.get("count")
|
|
747
|
-
attrs = d.get("attributes") or {}
|
|
748
|
-
token_type = attrs.get("type", "unknown") if isinstance(attrs, dict) else "unknown"
|
|
749
|
-
metric = finite_nonnegative_number(value, clamp_negative=True)
|
|
750
|
-
if metric is not None:
|
|
751
|
-
bucket = normalize_token_bucket(str(token_type))
|
|
752
|
-
local_tokens[bucket] += int(metric)
|
|
753
|
-
present_buckets.add(bucket)
|
|
754
|
-
|
|
755
|
-
for bucket in present_buckets:
|
|
756
|
-
summary.token_field_presence[bucket] += 1
|
|
757
|
-
if "cache_read" in present_buckets or "cache_creation" in present_buckets:
|
|
758
|
-
cache_telemetry_present = True
|
|
759
|
-
if local_tokens.get("cache_read", 0) > 0 or local_tokens.get("cache_creation", 0) > 0:
|
|
760
|
-
positive_cache_telemetry_present = True
|
|
761
|
-
|
|
762
|
-
if local_tokens:
|
|
763
|
-
summary.tokens.update(local_tokens)
|
|
764
|
-
record.tokens.update(local_tokens)
|
|
765
|
-
model = sanitize_label(first_string(d, MODEL_KEYS) or root_model or "unknown", 80)
|
|
766
|
-
query_source = sanitize_label(first_string(d, QUERY_SOURCE_KEYS) or root_query_source or "unknown", 80)
|
|
767
|
-
summary.by_model[model].update(local_tokens)
|
|
768
|
-
summary.by_query_source[query_source].update(local_tokens)
|
|
769
|
-
|
|
770
740
|
for key in COST_KEYS:
|
|
771
741
|
val = d.get(key)
|
|
772
742
|
metric = finite_nonnegative_number(val, clamp_negative=False)
|
|
@@ -776,17 +746,11 @@ def add_usage(
|
|
|
776
746
|
record.cost_usd += cost
|
|
777
747
|
summary.cost_field_count += 1
|
|
778
748
|
break
|
|
779
|
-
if parsed_timestamp is not None and cache_telemetry_present:
|
|
780
|
-
summary.cache_record_timestamps.append(parsed_timestamp)
|
|
781
|
-
if parsed_timestamp is not None and positive_cache_telemetry_present:
|
|
782
|
-
summary.positive_cache_record_timestamps.append(parsed_timestamp)
|
|
783
749
|
commands, tools = collect_record_hints(root, show_commands=show_commands)
|
|
784
750
|
record.commands = commands
|
|
785
751
|
record.tools = tools
|
|
786
|
-
|
|
787
|
-
if file is not None and (record_total or record.cost_usd):
|
|
752
|
+
if file is not None and record.cost_usd:
|
|
788
753
|
file_key = path_label(file, show_paths=show_paths)
|
|
789
|
-
summary.by_file[file_key] += record_total
|
|
790
754
|
summary.cost_by_file[file_key] += record.cost_usd
|
|
791
755
|
for command in commands:
|
|
792
756
|
summary.by_command[command] += 1
|
|
@@ -805,6 +769,42 @@ def parse_json_line(line: str) -> Any:
|
|
|
805
769
|
return json.loads(line)
|
|
806
770
|
|
|
807
771
|
|
|
772
|
+
def _apply_usage_reduction(
|
|
773
|
+
summary: UsageSummary,
|
|
774
|
+
reducer: UsageReducer,
|
|
775
|
+
row_metadata: dict[int, tuple[Path, str]],
|
|
776
|
+
*,
|
|
777
|
+
show_paths: bool,
|
|
778
|
+
) -> None:
|
|
779
|
+
reduction = reducer.finalize()
|
|
780
|
+
summary.usage_reducer_schema = reduction.schema
|
|
781
|
+
summary.usage_reducer_counters.update(reduction.counters)
|
|
782
|
+
summary.usage_reducer_partial = reduction.partial
|
|
783
|
+
summary.tokens.update(reduction.tokens)
|
|
784
|
+
for selection in reduction.selections:
|
|
785
|
+
local_tokens = Counter(selection.tokens)
|
|
786
|
+
for bucket in selection.present_buckets:
|
|
787
|
+
summary.token_field_presence[bucket] += 1
|
|
788
|
+
if local_tokens:
|
|
789
|
+
model = sanitize_label(selection.model, 80)
|
|
790
|
+
summary.by_model[model].update(local_tokens)
|
|
791
|
+
metadata = row_metadata.get(selection.row_ordinal)
|
|
792
|
+
if metadata is not None:
|
|
793
|
+
file, query_source = metadata
|
|
794
|
+
if local_tokens:
|
|
795
|
+
summary.by_query_source[query_source].update(local_tokens)
|
|
796
|
+
summary.by_file[path_label(file, show_paths=show_paths)] += sum(local_tokens.values())
|
|
797
|
+
cache_present = bool({"cache_read", "cache_creation"} & set(selection.present_buckets))
|
|
798
|
+
positive_cache = (
|
|
799
|
+
selection.tokens.get("cache_read", 0) > 0
|
|
800
|
+
or selection.tokens.get("cache_creation", 0) > 0
|
|
801
|
+
)
|
|
802
|
+
if selection.timestamp is not None and cache_present:
|
|
803
|
+
summary.cache_record_timestamps.append(selection.timestamp)
|
|
804
|
+
if selection.timestamp is not None and positive_cache:
|
|
805
|
+
summary.positive_cache_record_timestamps.append(selection.timestamp)
|
|
806
|
+
|
|
807
|
+
|
|
808
808
|
def scan(
|
|
809
809
|
paths: list[str],
|
|
810
810
|
show_paths: bool = False,
|
|
@@ -813,6 +813,38 @@ def scan(
|
|
|
813
813
|
) -> UsageSummary:
|
|
814
814
|
limits = limits or ScanLimits()
|
|
815
815
|
summary = UsageSummary()
|
|
816
|
+
reducer = UsageReducer()
|
|
817
|
+
row_metadata: dict[int, tuple[Path, str]] = {}
|
|
818
|
+
file_identities: dict[Path, str] = {}
|
|
819
|
+
next_ordinal = 0
|
|
820
|
+
|
|
821
|
+
def observe_row(file: Path, obj: Any, location: str) -> None:
|
|
822
|
+
nonlocal next_ordinal
|
|
823
|
+
if not isinstance(obj, dict):
|
|
824
|
+
summary.skipped_records += 1
|
|
825
|
+
reducer.note_invalid_row()
|
|
826
|
+
summary.note_error(
|
|
827
|
+
f"{path_label(file, show_paths=show_paths)}:{location}: "
|
|
828
|
+
"skipped non-object transcript row"
|
|
829
|
+
)
|
|
830
|
+
return
|
|
831
|
+
ordinal = next_ordinal
|
|
832
|
+
next_ordinal += 1
|
|
833
|
+
summary.records += 1
|
|
834
|
+
query_source = sanitize_label(first_string(obj, QUERY_SOURCE_KEYS) or "unknown", 80)
|
|
835
|
+
file_identity = file_identities.get(file)
|
|
836
|
+
if file_identity is None:
|
|
837
|
+
file_identity = hash_file_identity(file)
|
|
838
|
+
file_identities[file] = file_identity
|
|
839
|
+
accepted = reducer.observe(
|
|
840
|
+
obj,
|
|
841
|
+
file_identity=file_identity,
|
|
842
|
+
row_ordinal=ordinal,
|
|
843
|
+
)
|
|
844
|
+
if accepted:
|
|
845
|
+
row_metadata[ordinal] = (file, query_source)
|
|
846
|
+
add_usage(summary, obj, file, show_paths=show_paths, show_commands=show_commands)
|
|
847
|
+
|
|
816
848
|
for file in iter_jsonl_files(paths):
|
|
817
849
|
if summary.files >= limits.max_files:
|
|
818
850
|
summary.skipped_files += 1
|
|
@@ -834,9 +866,35 @@ def scan(
|
|
|
834
866
|
f"({size} bytes > {limits.max_file_bytes})"
|
|
835
867
|
)
|
|
836
868
|
continue
|
|
869
|
+
if file.suffix == ".json":
|
|
870
|
+
try:
|
|
871
|
+
raw = handle.read(size + 1)
|
|
872
|
+
parsed = parse_json_line(raw.decode("utf-8", errors="strict"))
|
|
873
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
874
|
+
summary.skipped_records += 1
|
|
875
|
+
reducer.note_invalid_row()
|
|
876
|
+
reason = "invalid UTF-8" if isinstance(exc, UnicodeDecodeError) else exc.msg
|
|
877
|
+
summary.note_error(
|
|
878
|
+
f"{path_label(file, show_paths=show_paths)}: JSON parse error: {reason}"
|
|
879
|
+
)
|
|
880
|
+
continue
|
|
881
|
+
except RecursionError:
|
|
882
|
+
summary.skipped_records += 1
|
|
883
|
+
reducer.note_invalid_row()
|
|
884
|
+
summary.note_error(
|
|
885
|
+
f"{path_label(file, show_paths=show_paths)}: "
|
|
886
|
+
"JSON parse error: nested JSON exceeds supported depth"
|
|
887
|
+
)
|
|
888
|
+
continue
|
|
889
|
+
rows = parsed if isinstance(parsed, list) else [parsed]
|
|
890
|
+
for index, obj in enumerate(rows):
|
|
891
|
+
observe_row(file, obj, f"item-{index}")
|
|
892
|
+
continue
|
|
893
|
+
|
|
837
894
|
for line_no, line in iter_bounded_lines(handle, limits.max_line_bytes):
|
|
838
895
|
if line is None:
|
|
839
896
|
summary.skipped_records += 1
|
|
897
|
+
reducer.note_invalid_row()
|
|
840
898
|
summary.note_error(
|
|
841
899
|
f"{path_label(file, show_paths=show_paths)}:{line_no}: "
|
|
842
900
|
f"skipped oversized JSONL record (> {limits.max_line_bytes} bytes)"
|
|
@@ -849,18 +907,26 @@ def scan(
|
|
|
849
907
|
obj = parse_json_line(line)
|
|
850
908
|
except json.JSONDecodeError as exc:
|
|
851
909
|
summary.skipped_records += 1
|
|
852
|
-
|
|
910
|
+
reducer.note_invalid_row()
|
|
911
|
+
summary.note_error(
|
|
912
|
+
f"{path_label(file, show_paths=show_paths)}:{line_no}: "
|
|
913
|
+
f"JSON parse error: {exc.msg}"
|
|
914
|
+
)
|
|
853
915
|
continue
|
|
854
|
-
except RecursionError
|
|
916
|
+
except RecursionError:
|
|
855
917
|
summary.skipped_records += 1
|
|
856
|
-
|
|
918
|
+
reducer.note_invalid_row()
|
|
919
|
+
summary.note_error(
|
|
920
|
+
f"{path_label(file, show_paths=show_paths)}:{line_no}: "
|
|
921
|
+
"JSON parse error: nested JSON exceeds supported depth"
|
|
922
|
+
)
|
|
857
923
|
continue
|
|
858
|
-
|
|
859
|
-
add_usage(summary, obj, file, show_paths=show_paths, show_commands=show_commands)
|
|
924
|
+
observe_row(file, obj, f"line-{line_no}")
|
|
860
925
|
except OSError as exc:
|
|
861
926
|
summary.skipped_files += 1
|
|
862
927
|
summary.note_error(f"{path_label(file, show_paths=show_paths)}: read error: {os_error_summary(exc)}")
|
|
863
928
|
continue
|
|
929
|
+
_apply_usage_reduction(summary, reducer, row_metadata, show_paths=show_paths)
|
|
864
930
|
return summary
|
|
865
931
|
|
|
866
932
|
|
|
@@ -933,7 +999,7 @@ def build_headroom_availability(summary: UsageSummary) -> dict[str, Any]:
|
|
|
933
999
|
|
|
934
1000
|
def scan_integrity(summary: UsageSummary) -> dict[str, Any]:
|
|
935
1001
|
skipped = summary.skipped_files + summary.skipped_records
|
|
936
|
-
complete = skipped == 0 and not summary.parse_errors
|
|
1002
|
+
complete = skipped == 0 and not summary.parse_errors and not summary.usage_reducer_partial
|
|
937
1003
|
return {
|
|
938
1004
|
"status": "complete" if complete else "partial",
|
|
939
1005
|
"files_scanned": summary.files,
|
|
@@ -943,6 +1009,15 @@ def scan_integrity(summary: UsageSummary) -> dict[str, Any]:
|
|
|
943
1009
|
"scan_truncated": summary.scan_truncated,
|
|
944
1010
|
"skipped_records": summary.skipped_records,
|
|
945
1011
|
"parse_error_count": len(summary.parse_errors),
|
|
1012
|
+
"usage_reducer_schema": summary.usage_reducer_schema,
|
|
1013
|
+
"usage_reducer_partial": summary.usage_reducer_partial,
|
|
1014
|
+
"usage_conflict": summary.usage_reducer_counters.get("usage_conflict", 0),
|
|
1015
|
+
"numeric_overflow": summary.usage_reducer_counters.get("numeric_overflow", 0),
|
|
1016
|
+
"invalid_numeric": summary.usage_reducer_counters.get("invalid_numeric", 0),
|
|
1017
|
+
"no_id_fallback": summary.usage_reducer_counters.get("no_id_fallback", 0),
|
|
1018
|
+
"ineligible_usage_shape": summary.usage_reducer_counters.get(
|
|
1019
|
+
"ineligible_usage_shape", 0
|
|
1020
|
+
),
|
|
946
1021
|
"complete": complete,
|
|
947
1022
|
"reason": (
|
|
948
1023
|
"All candidate transcript files/records were parsed within configured limits."
|
|
@@ -1987,7 +2062,8 @@ def build_recommendations(summary: UsageSummary, top: int) -> list[dict[str, Any
|
|
|
1987
2062
|
rec["confidence"] = finding.get("confidence")
|
|
1988
2063
|
recs.append(rec)
|
|
1989
2064
|
break
|
|
1990
|
-
|
|
2065
|
+
has_command_or_tool_evidence = bool(summary.by_command or summary.by_tool)
|
|
2066
|
+
if has_command_or_tool_evidence and (output_tokens >= 5_000 or output_ratio >= 0.35):
|
|
1991
2067
|
recs.append(recommendation(
|
|
1992
2068
|
"trim-output-heavy-sessions",
|
|
1993
2069
|
"Output tokens are a major hotspot",
|
|
@@ -2171,6 +2247,7 @@ def summary_json(
|
|
|
2171
2247
|
"scan_truncated": summary.scan_truncated,
|
|
2172
2248
|
"skipped_records": summary.skipped_records,
|
|
2173
2249
|
"parse_errors": summary.parse_errors,
|
|
2250
|
+
"scan_integrity": scan_integrity(summary),
|
|
2174
2251
|
"scan_limits": {
|
|
2175
2252
|
"max_file_bytes": limits.max_file_bytes,
|
|
2176
2253
|
"max_line_bytes": limits.max_line_bytes,
|
|
@@ -2178,6 +2255,24 @@ def summary_json(
|
|
|
2178
2255
|
},
|
|
2179
2256
|
"total_tokens": summary.total_tokens,
|
|
2180
2257
|
"tokens": dict(summary.tokens),
|
|
2258
|
+
"usage_reducer": {
|
|
2259
|
+
"schema": summary.usage_reducer_schema,
|
|
2260
|
+
"partial": summary.usage_reducer_partial,
|
|
2261
|
+
**{
|
|
2262
|
+
key: summary.usage_reducer_counters.get(key, 0)
|
|
2263
|
+
for key in (
|
|
2264
|
+
"observed_rows",
|
|
2265
|
+
"eligible_candidates",
|
|
2266
|
+
"selected_candidates",
|
|
2267
|
+
"usage_conflict",
|
|
2268
|
+
"numeric_overflow",
|
|
2269
|
+
"invalid_numeric",
|
|
2270
|
+
"invalid_row",
|
|
2271
|
+
"no_id_fallback",
|
|
2272
|
+
"ineligible_usage_shape",
|
|
2273
|
+
)
|
|
2274
|
+
},
|
|
2275
|
+
},
|
|
2181
2276
|
"cache_metrics": {
|
|
2182
2277
|
"cache_hit_rate": round(summary.cache_hit_rate, 4),
|
|
2183
2278
|
"cache_amortization": round(summary.cache_amortization, 4),
|
|
@@ -2282,6 +2377,14 @@ def main() -> int:
|
|
|
2282
2377
|
f"scan_limits=max_file_bytes:{limits.max_file_bytes} "
|
|
2283
2378
|
f"max_line_bytes:{limits.max_line_bytes} max_files:{limits.max_files}"
|
|
2284
2379
|
)
|
|
2380
|
+
print(
|
|
2381
|
+
f"usage_reducer={summary.usage_reducer_schema} "
|
|
2382
|
+
f"partial={str(summary.usage_reducer_partial).lower()} "
|
|
2383
|
+
f"conflicts={summary.usage_reducer_counters.get('usage_conflict', 0)} "
|
|
2384
|
+
f"overflows={summary.usage_reducer_counters.get('numeric_overflow', 0)} "
|
|
2385
|
+
f"no_id_fallback={summary.usage_reducer_counters.get('no_id_fallback', 0)} "
|
|
2386
|
+
f"ineligible_usage_shape={summary.usage_reducer_counters.get('ineligible_usage_shape', 0)}"
|
|
2387
|
+
)
|
|
2285
2388
|
print(f"observed_total_tokens={summary.total_tokens}")
|
|
2286
2389
|
if summary.cost_usd:
|
|
2287
2390
|
print(f"observed_cost_usd={summary.cost_usd:.4f}")
|