spanforge 1.0.0__py3-none-any.whl
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.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
spanforge/_cli_cost.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Cost command group for the SpanForge CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_cost_subcommands(sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> argparse.ArgumentParser:
|
|
13
|
+
"""Register cost-related CLI subcommands."""
|
|
14
|
+
cost_parser = sub.add_parser(
|
|
15
|
+
"cost",
|
|
16
|
+
help="Cost brief management",
|
|
17
|
+
)
|
|
18
|
+
cost_sub = cost_parser.add_subparsers(dest="cost_command", metavar="<action>")
|
|
19
|
+
|
|
20
|
+
brief_parser = cost_sub.add_parser("brief", help="Cost brief operations")
|
|
21
|
+
brief_sub = brief_parser.add_subparsers(dest="brief_command", metavar="<action>")
|
|
22
|
+
|
|
23
|
+
submit_parser = brief_sub.add_parser(
|
|
24
|
+
"submit",
|
|
25
|
+
help="Submit a cost brief JSON file to the local brief store",
|
|
26
|
+
)
|
|
27
|
+
submit_parser.add_argument(
|
|
28
|
+
"--file",
|
|
29
|
+
required=True,
|
|
30
|
+
metavar="BRIEF_JSON",
|
|
31
|
+
help="Path to a cost brief JSON file",
|
|
32
|
+
)
|
|
33
|
+
submit_parser.add_argument(
|
|
34
|
+
"--store",
|
|
35
|
+
default=".spanforge-cost-briefs.json",
|
|
36
|
+
metavar="STORE_JSON",
|
|
37
|
+
help="Path to the local cost brief store JSON file (default: .spanforge-cost-briefs.json)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
run_cost_parser = cost_sub.add_parser(
|
|
41
|
+
"run",
|
|
42
|
+
help="Show per-run cost breakdown for an agent run",
|
|
43
|
+
)
|
|
44
|
+
run_cost_parser.add_argument(
|
|
45
|
+
"--run-id",
|
|
46
|
+
required=True,
|
|
47
|
+
metavar="RUN_ID",
|
|
48
|
+
help="Agent run ID to look up",
|
|
49
|
+
)
|
|
50
|
+
run_cost_parser.add_argument(
|
|
51
|
+
"--input",
|
|
52
|
+
required=True,
|
|
53
|
+
metavar="JSONL",
|
|
54
|
+
help="Path to a JSONL events file to search",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return cost_parser
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def dispatch_cost_command(args: argparse.Namespace, cost_parser: argparse.ArgumentParser) -> int | None:
|
|
61
|
+
"""Dispatch cost-related commands when selected."""
|
|
62
|
+
if getattr(args, "command", None) != "cost":
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
cost_action = getattr(args, "cost_command", None)
|
|
66
|
+
brief_action = getattr(args, "brief_command", None)
|
|
67
|
+
if cost_action == "brief" and brief_action == "submit":
|
|
68
|
+
return _cmd_cost_brief_submit(args)
|
|
69
|
+
if cost_action == "run":
|
|
70
|
+
return _cmd_cost_run(args)
|
|
71
|
+
|
|
72
|
+
cost_parser.print_help()
|
|
73
|
+
return 2
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_cost_brief_store_json(store_path: Path) -> dict[str, Any]:
|
|
77
|
+
"""Load or initialise a JSON-file-backed cost brief store."""
|
|
78
|
+
if store_path.exists():
|
|
79
|
+
try:
|
|
80
|
+
data: dict[str, Any] = json.loads(store_path.read_text(encoding="utf-8"))
|
|
81
|
+
except (json.JSONDecodeError, OSError):
|
|
82
|
+
pass
|
|
83
|
+
else:
|
|
84
|
+
return data
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cmd_cost_brief_submit(args: argparse.Namespace) -> int:
|
|
89
|
+
"""Implement ``spanforge cost brief submit``."""
|
|
90
|
+
brief_path = Path(args.file)
|
|
91
|
+
if not brief_path.exists():
|
|
92
|
+
print(f"error: file not found: {brief_path}", file=sys.stderr)
|
|
93
|
+
return 2
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
brief_data = json.loads(brief_path.read_text(encoding="utf-8"))
|
|
97
|
+
except json.JSONDecodeError as exc:
|
|
98
|
+
print(f"error: invalid JSON in {brief_path}: {exc}", file=sys.stderr)
|
|
99
|
+
return 2
|
|
100
|
+
|
|
101
|
+
required = {"model_id", "submitted_by", "resource_config", "scenarios"}
|
|
102
|
+
missing = required - set(brief_data.keys())
|
|
103
|
+
if missing:
|
|
104
|
+
print(
|
|
105
|
+
f"error: cost brief missing required fields: {', '.join(sorted(missing))}",
|
|
106
|
+
file=sys.stderr,
|
|
107
|
+
)
|
|
108
|
+
return 2
|
|
109
|
+
|
|
110
|
+
store_path = Path(args.store)
|
|
111
|
+
store = _load_cost_brief_store_json(store_path)
|
|
112
|
+
|
|
113
|
+
from datetime import datetime, timezone
|
|
114
|
+
|
|
115
|
+
store[brief_data["model_id"]] = {
|
|
116
|
+
**brief_data,
|
|
117
|
+
"stored_at": datetime.now(timezone.utc).isoformat(),
|
|
118
|
+
}
|
|
119
|
+
store_path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
store_path.write_text(json.dumps(store, indent=2), encoding="utf-8")
|
|
121
|
+
|
|
122
|
+
print(f"[✓] Cost brief submitted model_id={brief_data['model_id']!r} store={store_path}")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cmd_cost_run(args: argparse.Namespace) -> int:
|
|
127
|
+
"""Implement ``spanforge cost run --run-id <id> --input <jsonl>``."""
|
|
128
|
+
run_id: str = args.run_id
|
|
129
|
+
events_path = Path(args.input)
|
|
130
|
+
|
|
131
|
+
if not events_path.exists():
|
|
132
|
+
print(f"error: file not found: {events_path}", file=sys.stderr)
|
|
133
|
+
return 2
|
|
134
|
+
|
|
135
|
+
cost_events: list[dict[str, Any]] = []
|
|
136
|
+
agent_run_event: dict[str, Any] | None = None
|
|
137
|
+
|
|
138
|
+
for line in events_path.read_text(encoding="utf-8").splitlines():
|
|
139
|
+
line = line.strip()
|
|
140
|
+
if not line:
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
event = json.loads(line)
|
|
144
|
+
except json.JSONDecodeError:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
payload = event.get("payload", {})
|
|
148
|
+
ns = event.get("namespace", "")
|
|
149
|
+
|
|
150
|
+
if ns.startswith("llm.cost.") and payload.get("agent_run_id") == run_id:
|
|
151
|
+
cost_events.append(event)
|
|
152
|
+
elif ns == "llm.trace.agent.completed" and payload.get("agent_run_id") == run_id:
|
|
153
|
+
agent_run_event = event
|
|
154
|
+
|
|
155
|
+
if not cost_events and agent_run_event is None:
|
|
156
|
+
print(f"error: no events found for run_id={run_id!r}", file=sys.stderr)
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
by_model: dict[str, dict[str, float]] = {}
|
|
160
|
+
total_usd = 0.0
|
|
161
|
+
total_input_tokens = 0
|
|
162
|
+
total_output_tokens = 0
|
|
163
|
+
|
|
164
|
+
for ev in cost_events:
|
|
165
|
+
payload = ev.get("payload", {})
|
|
166
|
+
cost_data = payload.get("cost", {})
|
|
167
|
+
model_data = payload.get("model", {})
|
|
168
|
+
model_name = (
|
|
169
|
+
model_data.get("name", "unknown") if isinstance(model_data, dict) else "unknown"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
input_cost = float(cost_data.get("input_cost_usd", 0.0))
|
|
173
|
+
output_cost = float(cost_data.get("output_cost_usd", 0.0))
|
|
174
|
+
total_cost = float(cost_data.get("total_cost_usd", 0.0))
|
|
175
|
+
|
|
176
|
+
token_data = payload.get("token_usage", {})
|
|
177
|
+
input_tokens = int(token_data.get("input_tokens", 0))
|
|
178
|
+
output_tokens = int(token_data.get("output_tokens", 0))
|
|
179
|
+
|
|
180
|
+
if model_name not in by_model:
|
|
181
|
+
by_model[model_name] = {
|
|
182
|
+
"input_cost": 0.0,
|
|
183
|
+
"output_cost": 0.0,
|
|
184
|
+
"total_cost": 0.0,
|
|
185
|
+
"input_tokens": 0,
|
|
186
|
+
"output_tokens": 0,
|
|
187
|
+
"calls": 0,
|
|
188
|
+
}
|
|
189
|
+
by_model[model_name]["input_cost"] += input_cost
|
|
190
|
+
by_model[model_name]["output_cost"] += output_cost
|
|
191
|
+
by_model[model_name]["total_cost"] += total_cost
|
|
192
|
+
by_model[model_name]["input_tokens"] += input_tokens
|
|
193
|
+
by_model[model_name]["output_tokens"] += output_tokens
|
|
194
|
+
by_model[model_name]["calls"] += 1
|
|
195
|
+
|
|
196
|
+
total_usd += total_cost
|
|
197
|
+
total_input_tokens += input_tokens
|
|
198
|
+
total_output_tokens += output_tokens
|
|
199
|
+
|
|
200
|
+
agent_name = "unknown"
|
|
201
|
+
run_status = "unknown"
|
|
202
|
+
run_duration_ms = 0.0
|
|
203
|
+
if agent_run_event:
|
|
204
|
+
run_payload = agent_run_event.get("payload", {})
|
|
205
|
+
agent_name = run_payload.get("agent_name", "unknown")
|
|
206
|
+
run_status = run_payload.get("status", "unknown")
|
|
207
|
+
run_duration_ms = float(run_payload.get("duration_ms", 0.0))
|
|
208
|
+
run_cost = run_payload.get("total_cost", {})
|
|
209
|
+
if run_cost:
|
|
210
|
+
total_usd = max(total_usd, float(run_cost.get("total_cost_usd", total_usd)))
|
|
211
|
+
|
|
212
|
+
lines: list[str] = []
|
|
213
|
+
lines.append("=" * 62)
|
|
214
|
+
lines.append(" SpanForge Per-Run Cost Report")
|
|
215
|
+
lines.append("=" * 62)
|
|
216
|
+
lines.append(f" Run ID : {run_id}")
|
|
217
|
+
lines.append(f" Agent : {agent_name}")
|
|
218
|
+
lines.append(f" Status : {run_status}")
|
|
219
|
+
if run_duration_ms > 0:
|
|
220
|
+
lines.append(f" Duration : {run_duration_ms:,.1f} ms")
|
|
221
|
+
lines.append(f" Total cost : ${total_usd:.6f}")
|
|
222
|
+
lines.append(f" Input tokens : {total_input_tokens:,}")
|
|
223
|
+
lines.append(f" Output tokens : {total_output_tokens:,}")
|
|
224
|
+
lines.append(f" LLM calls : {len(cost_events)}")
|
|
225
|
+
lines.append("-" * 62)
|
|
226
|
+
|
|
227
|
+
if by_model:
|
|
228
|
+
lines.append(" Cost by model:")
|
|
229
|
+
lines.append(
|
|
230
|
+
f" {'Model':<30s} {'Calls':>5s} {'Input $':>9s} {'Output $':>9s} {'Total $':>10s}"
|
|
231
|
+
)
|
|
232
|
+
lines.append(f" {'-' * 30} {'-' * 5} {'-' * 9} {'-' * 9} {'-' * 10}")
|
|
233
|
+
for model_name, data in sorted(
|
|
234
|
+
by_model.items(), key=lambda kv: kv[1]["total_cost"], reverse=True
|
|
235
|
+
):
|
|
236
|
+
lines.append(
|
|
237
|
+
f" {model_name:<30s} {int(data['calls']):>5d} "
|
|
238
|
+
f"${data['input_cost']:>8.6f} ${data['output_cost']:>8.6f} ${data['total_cost']:>9.6f}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
lines.append("=" * 62)
|
|
242
|
+
print("\n".join(lines))
|
|
243
|
+
return 0
|