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.
Files changed (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. 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()