velune-cli 0.9.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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Span context system for correlation IDs and trace propagation.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Unique run_id (UUID) for each orchestration run
|
|
5
|
+
- Span IDs for agent turns and tool executions
|
|
6
|
+
- Parent-child span relationships
|
|
7
|
+
- Automatic context binding via structlog
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextvars
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import AsyncIterator, Iterator
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import structlog
|
|
20
|
+
|
|
21
|
+
logger = structlog.get_logger()
|
|
22
|
+
|
|
23
|
+
# Context variables for span propagation
|
|
24
|
+
_run_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar("run_id", default=None)
|
|
25
|
+
_span_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar("span_id", default=None)
|
|
26
|
+
_parent_span_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
27
|
+
"parent_span_id", default=None
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SpanContext:
|
|
33
|
+
"""Represents a span context for tracing."""
|
|
34
|
+
|
|
35
|
+
run_id: str
|
|
36
|
+
span_id: str
|
|
37
|
+
parent_span_id: str | None = None
|
|
38
|
+
operation_name: str = ""
|
|
39
|
+
start_time: float = field(default_factory=lambda: __import__("time").time())
|
|
40
|
+
end_time: float | None = None
|
|
41
|
+
duration_ms: float | None = None
|
|
42
|
+
status: str = "running" # running, completed, failed
|
|
43
|
+
error: str | None = None
|
|
44
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict[str, Any]:
|
|
47
|
+
"""Convert to dictionary for logging."""
|
|
48
|
+
return {
|
|
49
|
+
"run_id": self.run_id,
|
|
50
|
+
"span_id": self.span_id,
|
|
51
|
+
"parent_span_id": self.parent_span_id,
|
|
52
|
+
"operation_name": self.operation_name,
|
|
53
|
+
"status": self.status,
|
|
54
|
+
"duration_ms": self.duration_ms,
|
|
55
|
+
"error": self.error,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_run_id() -> str:
|
|
60
|
+
"""Create a unique run ID for an orchestration run."""
|
|
61
|
+
return str(uuid.uuid4())[:8]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_span_id() -> str:
|
|
65
|
+
"""Create a unique span ID for an operation."""
|
|
66
|
+
return str(uuid.uuid4())[:8]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_current_run_id() -> str | None:
|
|
70
|
+
"""Get the current run ID from context."""
|
|
71
|
+
return _run_id_var.get()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_current_span_id() -> str | None:
|
|
75
|
+
"""Get the current span ID from context."""
|
|
76
|
+
return _span_id_var.get()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_current_parent_span_id() -> str | None:
|
|
80
|
+
"""Get the current parent span ID from context."""
|
|
81
|
+
return _parent_span_id_var.get()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@contextmanager
|
|
85
|
+
def span(
|
|
86
|
+
operation_name: str,
|
|
87
|
+
parent_run_id: str | None = None,
|
|
88
|
+
parent_span_id: str | None = None,
|
|
89
|
+
**attributes: Any,
|
|
90
|
+
) -> Iterator[SpanContext]:
|
|
91
|
+
"""Context manager for creating a new span.
|
|
92
|
+
|
|
93
|
+
Automatically binds span context to structlog for duration of operation.
|
|
94
|
+
Handles timing and status tracking.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
operation_name: Name of the operation (e.g., "coder_execution")
|
|
98
|
+
parent_run_id: Run ID for this span (creates new if None)
|
|
99
|
+
parent_span_id: Parent span ID (creates new span if None)
|
|
100
|
+
**attributes: Additional attributes to track
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
with span("coder_execution", parent_run_id=run_id) as span_ctx:
|
|
104
|
+
logger.info("Starting coder", operation=span_ctx.operation_name)
|
|
105
|
+
result = await coder.run()
|
|
106
|
+
# Span duration automatically tracked
|
|
107
|
+
"""
|
|
108
|
+
import time
|
|
109
|
+
|
|
110
|
+
# Create or use provided IDs
|
|
111
|
+
run_id = parent_run_id or get_current_run_id() or create_run_id()
|
|
112
|
+
span_id = create_span_id()
|
|
113
|
+
current_parent = parent_span_id or get_current_span_id()
|
|
114
|
+
|
|
115
|
+
# Create span context
|
|
116
|
+
span_ctx = SpanContext(
|
|
117
|
+
run_id=run_id,
|
|
118
|
+
span_id=span_id,
|
|
119
|
+
parent_span_id=current_parent,
|
|
120
|
+
operation_name=operation_name,
|
|
121
|
+
attributes=attributes,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Save old context
|
|
125
|
+
old_run_id = _run_id_var.get()
|
|
126
|
+
old_span_id = _span_id_var.get()
|
|
127
|
+
old_parent_span_id = _parent_span_id_var.get()
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Bind to context variables
|
|
131
|
+
_run_id_var.set(run_id)
|
|
132
|
+
_span_id_var.set(span_id)
|
|
133
|
+
_parent_span_id_var.set(current_parent)
|
|
134
|
+
|
|
135
|
+
# Bind to structlog
|
|
136
|
+
structlog.contextvars.bind_contextvars(
|
|
137
|
+
run_id=run_id,
|
|
138
|
+
span_id=span_id,
|
|
139
|
+
operation_name=operation_name,
|
|
140
|
+
**attributes,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Span started",
|
|
145
|
+
span_context=span_ctx.to_dict(),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
yield span_ctx
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
span_ctx.status = "failed"
|
|
152
|
+
span_ctx.error = str(e)
|
|
153
|
+
logger.error(
|
|
154
|
+
"Span failed",
|
|
155
|
+
span_context=span_ctx.to_dict(),
|
|
156
|
+
error=str(e),
|
|
157
|
+
)
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
finally:
|
|
161
|
+
# Record end time and duration
|
|
162
|
+
span_ctx.end_time = time.time()
|
|
163
|
+
span_ctx.duration_ms = (span_ctx.end_time - span_ctx.start_time) * 1000
|
|
164
|
+
|
|
165
|
+
if span_ctx.status == "running":
|
|
166
|
+
span_ctx.status = "completed"
|
|
167
|
+
|
|
168
|
+
logger.debug(
|
|
169
|
+
"Span ended",
|
|
170
|
+
span_context=span_ctx.to_dict(),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Unbind from structlog
|
|
174
|
+
unbind_keys = ["operation_name"] + list(attributes.keys())
|
|
175
|
+
structlog.contextvars.unbind_contextvars(*unbind_keys)
|
|
176
|
+
|
|
177
|
+
# Restore old context
|
|
178
|
+
if old_run_id is not None:
|
|
179
|
+
_run_id_var.set(old_run_id)
|
|
180
|
+
else:
|
|
181
|
+
_run_id_var.set(None)
|
|
182
|
+
|
|
183
|
+
if old_span_id is not None:
|
|
184
|
+
_span_id_var.set(old_span_id)
|
|
185
|
+
else:
|
|
186
|
+
_span_id_var.set(None)
|
|
187
|
+
|
|
188
|
+
if old_parent_span_id is not None:
|
|
189
|
+
_parent_span_id_var.set(old_parent_span_id)
|
|
190
|
+
else:
|
|
191
|
+
_parent_span_id_var.set(None)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def async_span(
|
|
195
|
+
operation_name: str,
|
|
196
|
+
parent_run_id: str | None = None,
|
|
197
|
+
parent_span_id: str | None = None,
|
|
198
|
+
**attributes: Any,
|
|
199
|
+
) -> AsyncIterator[SpanContext]:
|
|
200
|
+
"""Async context manager for creating a new span.
|
|
201
|
+
|
|
202
|
+
Same as span() but for async operations.
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
async with async_span("council_run", parent_run_id=run_id) as span_ctx:
|
|
206
|
+
result = await orchestrator.run()
|
|
207
|
+
"""
|
|
208
|
+
import time
|
|
209
|
+
|
|
210
|
+
run_id = parent_run_id or get_current_run_id() or create_run_id()
|
|
211
|
+
span_id = create_span_id()
|
|
212
|
+
current_parent = parent_span_id or get_current_span_id()
|
|
213
|
+
|
|
214
|
+
span_ctx = SpanContext(
|
|
215
|
+
run_id=run_id,
|
|
216
|
+
span_id=span_id,
|
|
217
|
+
parent_span_id=current_parent,
|
|
218
|
+
operation_name=operation_name,
|
|
219
|
+
attributes=attributes,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
old_run_id = _run_id_var.get()
|
|
223
|
+
old_span_id = _span_id_var.get()
|
|
224
|
+
old_parent_span_id = _parent_span_id_var.get()
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
_run_id_var.set(run_id)
|
|
228
|
+
_span_id_var.set(span_id)
|
|
229
|
+
_parent_span_id_var.set(current_parent)
|
|
230
|
+
|
|
231
|
+
structlog.contextvars.bind_contextvars(
|
|
232
|
+
run_id=run_id,
|
|
233
|
+
span_id=span_id,
|
|
234
|
+
operation_name=operation_name,
|
|
235
|
+
**attributes,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
logger.debug("Span started", span_context=span_ctx.to_dict())
|
|
239
|
+
|
|
240
|
+
yield span_ctx
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
span_ctx.status = "failed"
|
|
244
|
+
span_ctx.error = str(e)
|
|
245
|
+
logger.error(
|
|
246
|
+
"Span failed",
|
|
247
|
+
span_context=span_ctx.to_dict(),
|
|
248
|
+
error=str(e),
|
|
249
|
+
)
|
|
250
|
+
raise
|
|
251
|
+
|
|
252
|
+
finally:
|
|
253
|
+
span_ctx.end_time = time.time()
|
|
254
|
+
span_ctx.duration_ms = (span_ctx.end_time - span_ctx.start_time) * 1000
|
|
255
|
+
|
|
256
|
+
if span_ctx.status == "running":
|
|
257
|
+
span_ctx.status = "completed"
|
|
258
|
+
|
|
259
|
+
logger.debug("Span ended", span_context=span_ctx.to_dict())
|
|
260
|
+
|
|
261
|
+
unbind_keys = ["operation_name"] + list(attributes.keys())
|
|
262
|
+
structlog.contextvars.unbind_contextvars(*unbind_keys)
|
|
263
|
+
|
|
264
|
+
if old_run_id is not None:
|
|
265
|
+
_run_id_var.set(old_run_id)
|
|
266
|
+
else:
|
|
267
|
+
_run_id_var.set(None)
|
|
268
|
+
|
|
269
|
+
if old_span_id is not None:
|
|
270
|
+
_span_id_var.set(old_span_id)
|
|
271
|
+
else:
|
|
272
|
+
_span_id_var.set(None)
|
|
273
|
+
|
|
274
|
+
if old_parent_span_id is not None:
|
|
275
|
+
_parent_span_id_var.set(old_parent_span_id)
|
|
276
|
+
else:
|
|
277
|
+
_parent_span_id_var.set(None)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Token usage tracking and cost estimation for all inference calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Per-provider cost table (USD per 1 000 tokens, blended input+output rate).
|
|
10
|
+
# Zero means free tier.
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
PROVIDER_COSTS: dict[str, dict[str, float]] = {
|
|
13
|
+
"anthropic": {
|
|
14
|
+
"claude-opus-4-5": 0.015,
|
|
15
|
+
"claude-sonnet-4-5": 0.003,
|
|
16
|
+
"claude-haiku-4-5": 0.00025,
|
|
17
|
+
},
|
|
18
|
+
"openai": {
|
|
19
|
+
"gpt-4o": 0.005,
|
|
20
|
+
"gpt-4o-mini": 0.00015,
|
|
21
|
+
"o1": 0.015,
|
|
22
|
+
"o1-mini": 0.003,
|
|
23
|
+
},
|
|
24
|
+
"xai": {
|
|
25
|
+
"grok-2": 0.002,
|
|
26
|
+
"grok-2-mini": 0.0002,
|
|
27
|
+
},
|
|
28
|
+
"google": {
|
|
29
|
+
"gemini-2.0-flash": 0.000075,
|
|
30
|
+
"gemini-1.5-pro": 0.00125,
|
|
31
|
+
"gemini-1.5-flash": 0.000075,
|
|
32
|
+
"gemini-2.0-flash-thinking-exp": 0.0,
|
|
33
|
+
},
|
|
34
|
+
"groq": {
|
|
35
|
+
# All free tier
|
|
36
|
+
"llama-3.3-70b-versatile": 0.0,
|
|
37
|
+
"llama-3.1-8b-instant": 0.0,
|
|
38
|
+
"mixtral-8x7b-32768": 0.0,
|
|
39
|
+
"gemma2-9b-it": 0.0,
|
|
40
|
+
"llama-3.2-11b-vision-preview": 0.0,
|
|
41
|
+
},
|
|
42
|
+
"together": {
|
|
43
|
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo": 0.00088,
|
|
44
|
+
"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo": 0.00018,
|
|
45
|
+
"Qwen/Qwen2.5-Coder-32B-Instruct": 0.0008,
|
|
46
|
+
"deepseek-ai/DeepSeek-R1": 0.003,
|
|
47
|
+
"mistralai/Mistral-7B-Instruct-v0.3": 0.0002,
|
|
48
|
+
},
|
|
49
|
+
"fireworks": {
|
|
50
|
+
"accounts/fireworks/models/llama-v3p3-70b-instruct": 0.0009,
|
|
51
|
+
"accounts/fireworks/models/deepseek-r1": 0.003,
|
|
52
|
+
"accounts/fireworks/models/qwen2p5-coder-32b-instruct": 0.0009,
|
|
53
|
+
"accounts/fireworks/models/mixtral-8x22b-instruct": 0.0009,
|
|
54
|
+
},
|
|
55
|
+
"openrouter": {}, # Dynamic — set from model metadata
|
|
56
|
+
"ollama": {}, # Always free (local)
|
|
57
|
+
"lmstudio": {}, # Always free (local)
|
|
58
|
+
"llamacpp": {}, # Always free (local)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class TokenUsage:
|
|
64
|
+
model_id: str
|
|
65
|
+
provider_id: str
|
|
66
|
+
prompt_tokens: int
|
|
67
|
+
completion_tokens: int
|
|
68
|
+
total_tokens: int
|
|
69
|
+
cost_usd: float
|
|
70
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_response(
|
|
74
|
+
cls,
|
|
75
|
+
provider_id: str,
|
|
76
|
+
model_id: str,
|
|
77
|
+
prompt_tokens: int,
|
|
78
|
+
completion_tokens: int,
|
|
79
|
+
) -> TokenUsage:
|
|
80
|
+
total = prompt_tokens + completion_tokens
|
|
81
|
+
cost = cls._calculate_cost(provider_id, model_id, total)
|
|
82
|
+
return cls(
|
|
83
|
+
model_id=model_id,
|
|
84
|
+
provider_id=provider_id,
|
|
85
|
+
prompt_tokens=prompt_tokens,
|
|
86
|
+
completion_tokens=completion_tokens,
|
|
87
|
+
total_tokens=total,
|
|
88
|
+
cost_usd=cost,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _calculate_cost(provider_id: str, model_id: str, tokens: int) -> float:
|
|
93
|
+
rate = PROVIDER_COSTS.get(provider_id, {}).get(model_id, 0.0)
|
|
94
|
+
return (tokens / 1000) * rate
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class SessionUsage:
|
|
99
|
+
usages: list[TokenUsage] = field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
def add(self, usage: TokenUsage) -> None:
|
|
102
|
+
self.usages.append(usage)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def total_tokens(self) -> int:
|
|
106
|
+
return sum(u.total_tokens for u in self.usages)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def total_cost(self) -> float:
|
|
110
|
+
return sum(u.cost_usd for u in self.usages)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def prompt_tokens(self) -> int:
|
|
114
|
+
return sum(u.prompt_tokens for u in self.usages)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def completion_tokens(self) -> int:
|
|
118
|
+
return sum(u.completion_tokens for u in self.usages)
|
|
119
|
+
|
|
120
|
+
def by_provider(self) -> dict[str, int]:
|
|
121
|
+
result: dict[str, int] = {}
|
|
122
|
+
for u in self.usages:
|
|
123
|
+
result[u.provider_id] = result.get(u.provider_id, 0) + u.total_tokens
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def summary_line(self) -> str:
|
|
127
|
+
parts = [f"{self.total_tokens:,} tokens"]
|
|
128
|
+
if self.total_cost > 0:
|
|
129
|
+
parts.append(f"~${self.total_cost:.4f}")
|
|
130
|
+
else:
|
|
131
|
+
parts.append("free")
|
|
132
|
+
parts.append("session total")
|
|
133
|
+
return " · ".join(parts)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Process-level session tracker — populated by inference call sites
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
current_session: SessionUsage = SessionUsage()
|