alysis-code 0.13.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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,1990 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import math
6
+ import threading
7
+ from collections.abc import Iterable, Mapping
8
+ from dataclasses import dataclass
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from statistics import median
12
+ from typing import Any
13
+
14
+ from .llm.metadata import strip_provider_metadata_from_message
15
+ from .llm.types import BillingMode, CostSource, UsageConfidence, UsageContract, UsageSource
16
+ from .model_registry import ModelMeta, ModelRegistry
17
+ from .provider_telemetry import base_url_host
18
+ from .request_estimation import (
19
+ RequestTokenBreakdown,
20
+ estimate_request_token_breakdown,
21
+ request_contains_media,
22
+ request_message_signatures,
23
+ tool_schema_signature,
24
+ )
25
+ from .session_store import read_session_events
26
+ from .token_budget import compute_input_budget, estimate_tokens
27
+
28
+
29
+ def now_iso() -> str:
30
+ return datetime.now(UTC).replace(microsecond=0).isoformat()
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class UsageRecord:
35
+ timestamp: str
36
+ role: str
37
+ requested_model: str
38
+ response_model: str | None
39
+ prompt_tokens: int
40
+ completion_tokens: int
41
+ total_tokens: int
42
+ input_cost_per_token: float | None
43
+ output_cost_per_token: float | None
44
+ cost_usd: float | None
45
+ usage_source: str
46
+ billing_mode: str = BillingMode.METERED_API.value
47
+ cost_source: str = CostSource.UNKNOWN.value
48
+ provider_cost_usd: float | None = None
49
+ usage_source_detail: str = UsageSource.LOCAL_ESTIMATE.value
50
+ usage_confidence: str = UsageConfidence.ESTIMATED.value
51
+ output_includes_reasoning: bool = True
52
+ usage_schema_version: int = 6
53
+ provider_key: str | None = None
54
+ protocol: str | None = None
55
+ base_url_host: str | None = None
56
+ operation: str | None = None
57
+ request_mode: str | None = None
58
+ cache_strategy: str | None = None
59
+ request_plan: dict[str, Any] | None = None
60
+ request_has_media: bool = False
61
+ cache_read_input_cost_per_token: float | None = None
62
+ cache_creation_input_cost_per_token: float | None = None
63
+ cache_creation_5m_input_cost_per_token: float | None = None
64
+ cache_creation_1h_input_cost_per_token: float | None = None
65
+ reasoning_output_cost_per_token: float | None = None
66
+ cached_prompt_tokens: int | None = None
67
+ uncached_prompt_tokens: int | None = None
68
+ input_tokens_uncached: int | None = None
69
+ input_tokens_uncached_derived: bool = False
70
+ cache_read_input_tokens: int | None = None
71
+ cache_creation_input_tokens: int | None = None
72
+ cache_creation_5m_input_tokens: int | None = None
73
+ cache_creation_1h_input_tokens: int | None = None
74
+ reasoning_tokens: int | None = None
75
+ raw_provider_usage: dict[str, Any] | None = None
76
+ cache_cost_pricing_missing: bool = False
77
+ request_token_estimate: RequestTokenBreakdown | None = None
78
+ prompt_estimate_tokens: int | None = None
79
+ prompt_estimate_error_tokens: int | None = None
80
+ prompt_estimate_error_ratio: float | None = None
81
+ raw_api_prompt_tokens: int | None = None
82
+ raw_api_completion_tokens: int | None = None
83
+ raw_api_total_tokens: int | None = None
84
+ usage_correction_reason: str | None = None
85
+
86
+ def to_payload(self) -> dict[str, Any]:
87
+ return {
88
+ "event_type": "llm_usage",
89
+ "usage_schema_version": self.usage_schema_version,
90
+ "timestamp": self.timestamp,
91
+ "role": self.role,
92
+ "requested_model": self.requested_model,
93
+ "response_model": self.response_model,
94
+ "prompt_tokens": self.prompt_tokens,
95
+ "completion_tokens": self.completion_tokens,
96
+ "total_tokens": self.total_tokens,
97
+ "input_cost_per_token": self.input_cost_per_token,
98
+ "output_cost_per_token": self.output_cost_per_token,
99
+ "cache_read_input_cost_per_token": self.cache_read_input_cost_per_token,
100
+ "cache_creation_input_cost_per_token": self.cache_creation_input_cost_per_token,
101
+ "cache_creation_5m_input_cost_per_token": (self.cache_creation_5m_input_cost_per_token),
102
+ "cache_creation_1h_input_cost_per_token": (self.cache_creation_1h_input_cost_per_token),
103
+ "reasoning_output_cost_per_token": self.reasoning_output_cost_per_token,
104
+ "cost_usd": self.cost_usd,
105
+ "billing_mode": self.billing_mode,
106
+ "cost_source": self.cost_source,
107
+ "provider_cost_usd": self.provider_cost_usd,
108
+ "usage_source": self.usage_source,
109
+ "usage_source_detail": self.usage_source_detail,
110
+ "usage_confidence": self.usage_confidence,
111
+ "output_includes_reasoning": self.output_includes_reasoning,
112
+ "provider_key": self.provider_key,
113
+ "protocol": self.protocol,
114
+ "base_url_host": self.base_url_host,
115
+ "operation": self.operation,
116
+ "request_mode": self.request_mode,
117
+ "cache_strategy": self.cache_strategy,
118
+ "request_plan": copy.deepcopy(self.request_plan),
119
+ "request_has_media": self.request_has_media,
120
+ "cached_prompt_tokens": self.cached_prompt_tokens,
121
+ "uncached_prompt_tokens": self.uncached_prompt_tokens,
122
+ "input_tokens_uncached": self.input_tokens_uncached,
123
+ "input_tokens_uncached_derived": self.input_tokens_uncached_derived,
124
+ "cache_read_input_tokens": self.cache_read_input_tokens,
125
+ "cache_creation_input_tokens": self.cache_creation_input_tokens,
126
+ "cache_creation_5m_input_tokens": self.cache_creation_5m_input_tokens,
127
+ "cache_creation_1h_input_tokens": self.cache_creation_1h_input_tokens,
128
+ "reasoning_tokens": self.reasoning_tokens,
129
+ "raw_provider_usage": copy.deepcopy(self.raw_provider_usage),
130
+ "cache_cost_pricing_missing": self.cache_cost_pricing_missing,
131
+ "request_token_estimate": (
132
+ self.request_token_estimate.to_payload()
133
+ if self.request_token_estimate is not None
134
+ else None
135
+ ),
136
+ "prompt_estimate_tokens": self.prompt_estimate_tokens,
137
+ "prompt_estimate_error_tokens": self.prompt_estimate_error_tokens,
138
+ "prompt_estimate_error_ratio": self.prompt_estimate_error_ratio,
139
+ "raw_api_prompt_tokens": self.raw_api_prompt_tokens,
140
+ "raw_api_completion_tokens": self.raw_api_completion_tokens,
141
+ "raw_api_total_tokens": self.raw_api_total_tokens,
142
+ "usage_correction_reason": self.usage_correction_reason,
143
+ }
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class ContextLeft:
148
+ model_name: str
149
+ max_input_tokens: int | None
150
+ used_input_tokens: int
151
+ remaining_tokens: int | None
152
+ percent_left: float | None
153
+ source: str
154
+ context_window_tokens: int | None = None
155
+ context_window_remaining_tokens: int | None = None
156
+ context_window_percent_left: float | None = None
157
+ effective_input_budget: int | None = None
158
+ effective_remaining_tokens: int | None = None
159
+ effective_percent_left: float | None = None
160
+ startup_baseline_tokens: int = 0
161
+ dynamic_context_budget_tokens: int | None = None
162
+ dynamic_context_used_tokens: int = 0
163
+ dynamic_context_remaining_tokens: int | None = None
164
+ dynamic_context_percent_left: float | None = None
165
+ token_count_source: str = UsageSource.LOCAL_ESTIMATE.value
166
+ token_count_confidence: str = UsageConfidence.ESTIMATED.value
167
+ local_request_estimate_tokens: int = 0
168
+ anchor_token_count_source: str | None = None
169
+ anchor_token_count_confidence: str | None = None
170
+ provider_projection_applied: bool = False
171
+ capacity_provider_key: str | None = None
172
+ context_window_source: str | None = None
173
+ max_output_tokens: int | None = None
174
+ max_output_source: str | None = None
175
+ safety_margin_tokens: int = 0
176
+
177
+
178
+ @dataclass(frozen=True)
179
+ class RequestContextMeasurement:
180
+ """Provider-visible input measurement anchored to persistent session state.
181
+
182
+ ``input_tokens`` measures the fully assembled provider request, including
183
+ ephemeral controller messages. ``anchor_estimate_tokens`` is the local
184
+ estimate of that same full request. ``persistent_anchor_estimate_tokens``
185
+ is the local estimate of durable session state at that instant, allowing
186
+ the HUD to add later persistent growth without losing recurring ephemeral
187
+ request overhead.
188
+ """
189
+
190
+ input_tokens: int
191
+ anchor_estimate_tokens: int
192
+ source: str
193
+ confidence: str
194
+ persistent_anchor_estimate_tokens: int = 0
195
+ requested_model: str = ""
196
+ provider_key: str | None = None
197
+ protocol: str | None = None
198
+ base_url_host: str | None = None
199
+ operation: str | None = None
200
+ request_mode: str | None = None
201
+ cache_strategy: str | None = None
202
+ request_message_signatures: tuple[str, ...] = ()
203
+ persistent_message_signatures: tuple[str, ...] = ()
204
+ tool_schema_signature: str = ""
205
+ request_has_media: bool = False
206
+ persistent_has_media: bool = False
207
+
208
+ def __post_init__(self) -> None:
209
+ for field_name, value in (
210
+ ("input_tokens", self.input_tokens),
211
+ ("anchor_estimate_tokens", self.anchor_estimate_tokens),
212
+ ("persistent_anchor_estimate_tokens", self.persistent_anchor_estimate_tokens),
213
+ ):
214
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
215
+ raise ValueError(f"{field_name} must be a non-negative integer")
216
+
217
+ def matches_route(
218
+ self,
219
+ *,
220
+ requested_model: str,
221
+ provider_key: str | None,
222
+ protocol: str | None,
223
+ base_url_host: str | None,
224
+ ) -> bool:
225
+ expected = (
226
+ (self.requested_model, requested_model),
227
+ (self.provider_key, provider_key),
228
+ (self.protocol, protocol),
229
+ (self.base_url_host, base_url_host),
230
+ )
231
+ return all(not left or str(left) == str(right or "") for left, right in expected)
232
+
233
+ def projection_kind(
234
+ self,
235
+ *,
236
+ messages: list[dict[str, Any]],
237
+ tool_list: list[dict[str, Any]] | None,
238
+ ) -> str | None:
239
+ current = request_message_signatures(messages)
240
+ if not self.request_message_signatures or self.anchor_estimate_tokens <= 0:
241
+ return None
242
+ if tool_schema_signature(tool_list) != self.tool_schema_signature:
243
+ return None
244
+ if current == self.request_message_signatures:
245
+ return "exact_request"
246
+ prefix = self.persistent_message_signatures
247
+ if (
248
+ not prefix
249
+ or self.persistent_anchor_estimate_tokens <= 0
250
+ or (self.request_has_media and not self.persistent_has_media)
251
+ or len(current) < len(prefix)
252
+ or current[: len(prefix)] != prefix
253
+ ):
254
+ return None
255
+ if request_contains_media(messages[len(prefix) :]):
256
+ return None
257
+ return "append_only_projection"
258
+
259
+
260
+ @dataclass
261
+ class _ModelUsageTotals:
262
+ prompt_tokens: int = 0
263
+ completion_tokens: int = 0
264
+ total_tokens: int = 0
265
+ known_cost_usd: float = 0.0
266
+ known_cost_calls: int = 0
267
+ unknown_cost_count: int = 0
268
+ provider_reported_cost_calls: int = 0
269
+ catalog_estimated_cost_calls: int = 0
270
+ subscription_calls: int = 0
271
+ included_calls: int = 0
272
+ local_calls: int = 0
273
+ api_usage_calls: int = 0
274
+ estimate_usage_calls: int = 0
275
+ authoritative_usage_calls: int = 0
276
+ reported_usage_calls: int = 0
277
+ cached_prompt_tokens: int = 0
278
+ uncached_prompt_tokens: int = 0
279
+ input_tokens_uncached: int = 0
280
+ cache_read_input_tokens: int = 0
281
+ cache_creation_input_tokens: int = 0
282
+ cache_creation_5m_input_tokens: int = 0
283
+ cache_creation_1h_input_tokens: int = 0
284
+ input_tokens_uncached_reported_calls: int = 0
285
+ input_tokens_uncached_derived_calls: int = 0
286
+ cache_read_reported_calls: int = 0
287
+ cache_creation_reported_calls: int = 0
288
+ reasoning_tokens: int = 0
289
+ cache_cost_pricing_missing_calls: int = 0
290
+ estimated_bootstrap_prompt_tokens: int = 0
291
+ estimated_tool_schema_tokens: int = 0
292
+ estimated_live_conversation_history_tokens: int = 0
293
+ estimated_inline_tool_transcript_tokens: int = 0
294
+ estimated_memory_summary_tokens: int = 0
295
+ estimated_pins_tokens: int = 0
296
+ estimated_total_request_tokens: int = 0
297
+ tool_schema_budget_reported_calls: int = 0
298
+ tool_schema_budget_exceeded_calls: int = 0
299
+ tool_schema_budget_overage_tokens: int = 0
300
+ tool_schema_largest_tool_tokens: int = 0
301
+ corrected_usage_calls: int = 0
302
+ prompt_estimate_calibration_calls: int = 0
303
+ prompt_estimate_error_ratio_sum: float = 0.0
304
+ prompt_estimate_error_ratio_max: float = 0.0
305
+ prompt_estimate_underestimate_calls: int = 0
306
+ prompt_estimate_overestimate_calls: int = 0
307
+
308
+
309
+ def _calibration_group_key(record: UsageRecord) -> tuple[str, str, str, str, str, str, str]:
310
+ return (
311
+ record.requested_model.strip() or "unknown-model",
312
+ record.provider_key or "",
313
+ record.protocol or "",
314
+ record.base_url_host or "",
315
+ record.operation or "",
316
+ record.request_mode or "",
317
+ record.cache_strategy or "",
318
+ )
319
+
320
+
321
+ def _record_matches_calibration_filters(
322
+ record: UsageRecord,
323
+ *,
324
+ requested_model: str,
325
+ provider_key: str,
326
+ protocol: str,
327
+ base_url_host_filter: str,
328
+ operation: str,
329
+ request_mode: str,
330
+ cache_strategy: str,
331
+ ) -> bool:
332
+ return (
333
+ (not requested_model or record.requested_model.strip() == requested_model)
334
+ and (not provider_key or (record.provider_key or "") == provider_key)
335
+ and (not protocol or (record.protocol or "") == protocol)
336
+ and (not base_url_host_filter or (record.base_url_host or "") == base_url_host_filter)
337
+ and (not operation or (record.operation or "") == operation)
338
+ and (not request_mode or (record.request_mode or "") == request_mode)
339
+ and (not cache_strategy or (record.cache_strategy or "") == cache_strategy)
340
+ )
341
+
342
+
343
+ def _messages_without_provider_metadata(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
344
+ return [
345
+ strip_provider_metadata_from_message(message)
346
+ for message in messages
347
+ if isinstance(message, dict)
348
+ ]
349
+
350
+
351
+ def estimate_prompt_tokens(messages: list[dict[str, Any]]) -> int:
352
+ if not messages:
353
+ return 0
354
+ serialized = json.dumps(
355
+ _messages_without_provider_metadata(messages),
356
+ ensure_ascii=False,
357
+ sort_keys=True,
358
+ )
359
+ return estimate_tokens(serialized)
360
+
361
+
362
+ def _completion_payload_for_estimation(content: str, tool_calls: list[Any]) -> str:
363
+ if content.strip():
364
+ base = content
365
+ else:
366
+ base = ""
367
+ if tool_calls:
368
+ base += "\n" + json.dumps(tool_calls, ensure_ascii=False, sort_keys=True)
369
+ return base
370
+
371
+
372
+ def estimate_completion_tokens(content: str, tool_calls: list[Any]) -> int:
373
+ base = _completion_payload_for_estimation(content, tool_calls)
374
+ if not base.strip():
375
+ return 0
376
+ return estimate_tokens(base)
377
+
378
+
379
+ def _prompt_character_count(
380
+ messages: list[dict[str, Any]],
381
+ tool_list: list[dict[str, Any]] | None,
382
+ ) -> int:
383
+ payload: dict[str, Any] = {"messages": _messages_without_provider_metadata(messages)}
384
+ if tool_list:
385
+ payload["tools"] = tool_list
386
+ return len(json.dumps(payload, ensure_ascii=False, sort_keys=True))
387
+
388
+
389
+ def _looks_like_character_count(
390
+ *,
391
+ api_count: int | None,
392
+ estimated_tokens: int,
393
+ character_count: int,
394
+ ) -> bool:
395
+ if api_count is None or api_count <= 0 or estimated_tokens <= 0:
396
+ return False
397
+ if character_count <= 0:
398
+ return api_count >= max(estimated_tokens * 3, estimated_tokens + 256)
399
+
400
+ # Some OpenAI-compatible providers put character counts in token fields.
401
+ # Trust plausible API token counts, but replace counts that are much closer
402
+ # to raw text size than to the local tokenizer estimate.
403
+ minimum_delta = 64 if character_count < 2048 else 256
404
+ far_above_tokens = api_count >= max(estimated_tokens * 2.75, estimated_tokens + minimum_delta)
405
+ close_to_chars = abs(api_count - character_count) <= max(16, int(character_count * 0.15))
406
+ return far_above_tokens and close_to_chars
407
+
408
+
409
+ def _safe_int(value: Any) -> int | None:
410
+ try:
411
+ parsed = int(value)
412
+ except (TypeError, ValueError):
413
+ return None
414
+ if parsed < 0:
415
+ return None
416
+ return parsed
417
+
418
+
419
+ def _safe_float(value: Any) -> float | None:
420
+ try:
421
+ parsed = float(value)
422
+ except (TypeError, ValueError):
423
+ return None
424
+ return parsed
425
+
426
+
427
+ def _safe_bool(value: Any) -> bool:
428
+ if isinstance(value, bool):
429
+ return value
430
+ text = str(value or "").strip().lower()
431
+ return text in {"1", "true", "yes", "on"}
432
+
433
+
434
+ def _safe_label(value: Any) -> str | None:
435
+ text = str(value or "").strip()
436
+ return text or None
437
+
438
+
439
+ def _safe_request_plan_payload(payload: Any) -> dict[str, Any] | None:
440
+ if not isinstance(payload, Mapping):
441
+ return None
442
+ safe: dict[str, Any] = {}
443
+ for key in (
444
+ "input_mode",
445
+ "continuation_strategy",
446
+ "cache_strategy",
447
+ "cache_mode",
448
+ "cacheable_prefix_hash",
449
+ "request_messages_signature",
450
+ "tool_schema_hash",
451
+ ):
452
+ value = _safe_label(payload.get(key))
453
+ if value is not None:
454
+ safe[key] = value
455
+ for key in ("previous_response_id_used", "fallback_used", "stream"):
456
+ if key in payload:
457
+ safe[key] = bool(payload.get(key))
458
+ for key in (
459
+ "schema_version",
460
+ "message_count",
461
+ "request_message_count",
462
+ "tool_count",
463
+ "stable_prefix_message_count",
464
+ "dynamic_suffix_message_count",
465
+ "provider_metadata_message_count",
466
+ "stable_prefix_estimated_tokens",
467
+ "dynamic_suffix_estimated_tokens",
468
+ "tool_schema_tokens",
469
+ "total_estimated_tokens",
470
+ "serialized_request_estimate_tokens",
471
+ "sent_serialized_request_estimate_tokens",
472
+ "full_input_item_count",
473
+ "sent_input_item_count",
474
+ "continuation_anchor_index",
475
+ "resent_stable_instruction_count",
476
+ ):
477
+ value = _safe_int(payload.get(key))
478
+ if value is not None:
479
+ safe[key] = value
480
+ return safe or None
481
+
482
+
483
+ def _request_plan_from_response(response: Any | None) -> dict[str, Any] | None:
484
+ metadata = getattr(response, "provider_metadata", None)
485
+ if not isinstance(metadata, Mapping):
486
+ return None
487
+ direct = _safe_request_plan_payload(metadata.get("request_plan"))
488
+ if direct is not None:
489
+ return direct
490
+ for value in metadata.values():
491
+ if not isinstance(value, Mapping):
492
+ continue
493
+ nested = _safe_request_plan_payload(value.get("request_plan"))
494
+ if nested is not None:
495
+ return nested
496
+ return None
497
+
498
+
499
+ def _cache_strategy_from_response(response: Any | None) -> str | None:
500
+ metadata = getattr(response, "provider_metadata", None)
501
+ if not isinstance(metadata, Mapping):
502
+ return None
503
+ for value in metadata.values():
504
+ if not isinstance(value, Mapping):
505
+ continue
506
+ cache_policy = value.get("cache_policy")
507
+ if isinstance(cache_policy, Mapping):
508
+ strategy = _safe_label(cache_policy.get("strategy"))
509
+ if strategy is not None:
510
+ return strategy
511
+ return None
512
+
513
+
514
+ def _protocol_from_client(client: Any | None) -> str | None:
515
+ for attr in ("protocol", "provider_protocol", "protocol_name"):
516
+ value = _safe_label(getattr(client, attr, None))
517
+ if value is not None:
518
+ return value
519
+ module = str(getattr(getattr(client, "__class__", None), "__module__", "") or "")
520
+ leaf = module.rsplit(".", 1)[-1]
521
+ if leaf in {
522
+ "openai_responses",
523
+ "openai_compat",
524
+ "anthropic_messages",
525
+ "gemini_generate_content",
526
+ "gemini_interactions",
527
+ }:
528
+ return leaf
529
+ class_name = str(getattr(getattr(client, "__class__", None), "__name__", "") or "")
530
+ if class_name:
531
+ return class_name
532
+ return None
533
+
534
+
535
+ def usage_context_from_client_response(
536
+ *,
537
+ client: Any | None,
538
+ response: Any | None,
539
+ operation: str,
540
+ request_plan: Mapping[str, Any] | None = None,
541
+ cache_strategy: str | None = None,
542
+ request_mode: str | None = None,
543
+ ) -> dict[str, Any]:
544
+ safe_plan = _safe_request_plan_payload(request_plan) or _request_plan_from_response(response)
545
+ inferred_cache_strategy = (
546
+ _safe_label(cache_strategy)
547
+ or (
548
+ str(safe_plan.get("cache_strategy"))
549
+ if safe_plan and safe_plan.get("cache_strategy")
550
+ else None
551
+ )
552
+ or _cache_strategy_from_response(response)
553
+ )
554
+ inferred_request_mode = (
555
+ _safe_label(request_mode)
556
+ or (str(safe_plan.get("input_mode")) if safe_plan and safe_plan.get("input_mode") else None)
557
+ or "full"
558
+ )
559
+ usage_contract = getattr(client, "usage_contract", None)
560
+ if not isinstance(usage_contract, UsageContract):
561
+ usage_contract = UsageContract(
562
+ response_usage_confidence=(
563
+ UsageConfidence.AUTHORITATIVE
564
+ if bool(getattr(client, "usage_counts_authoritative", False))
565
+ else UsageConfidence.REPORTED
566
+ )
567
+ )
568
+ contract_billing_mode = getattr(usage_contract, "billing_mode", BillingMode.METERED_API)
569
+ billing_mode_value = str(
570
+ getattr(contract_billing_mode, "value", contract_billing_mode)
571
+ or BillingMode.METERED_API.value
572
+ )
573
+ return {
574
+ "provider_key": _safe_label(getattr(client, "provider_key", None)),
575
+ "protocol": _protocol_from_client(client),
576
+ "base_url_host": base_url_host(getattr(client, "base_url", None)),
577
+ "operation": _safe_label(operation),
578
+ "request_mode": inferred_request_mode,
579
+ "cache_strategy": inferred_cache_strategy,
580
+ "request_plan": safe_plan,
581
+ "api_usage_counts_authoritative": usage_contract.response_usage_authoritative,
582
+ "api_prompt_tokens_authoritative": usage_contract.response_usage_authoritative,
583
+ "api_usage_confidence": usage_contract.response_usage_confidence.value,
584
+ "api_usage_source_detail": UsageSource.PROVIDER_RESPONSE.value,
585
+ "api_output_includes_reasoning": (usage_contract.normalized_output_includes_reasoning),
586
+ "billing_mode": billing_mode_value,
587
+ }
588
+
589
+
590
+ def _prompt_estimate_calibration(
591
+ *,
592
+ api_prompt_tokens: int | None,
593
+ estimated_prompt_tokens: int,
594
+ corrected_fields: list[str],
595
+ usage_source: str,
596
+ api_prompt_tokens_authoritative: bool = False,
597
+ ) -> tuple[int | None, float | None]:
598
+ if (
599
+ api_prompt_tokens is None
600
+ or estimated_prompt_tokens <= 0
601
+ or (usage_source != "api" and not api_prompt_tokens_authoritative)
602
+ or "prompt_tokens" in corrected_fields
603
+ ):
604
+ return None, None
605
+ error_tokens = api_prompt_tokens - estimated_prompt_tokens
606
+ return error_tokens, api_prompt_tokens / max(1, estimated_prompt_tokens)
607
+
608
+
609
+ def _robust_calibration_ratios(values: Iterable[float]) -> list[float]:
610
+ """Return a coherent route-local sample without imposing a universal cap."""
611
+
612
+ ratios = [float(value) for value in values if math.isfinite(value) and value > 0]
613
+ if len(ratios) < 3:
614
+ return []
615
+ center = float(median(ratios))
616
+ deviations = [abs(value - center) for value in ratios]
617
+ mad = float(median(deviations))
618
+ # An evenly split or otherwise high-spread sample has no defensible center.
619
+ # Reject it instead of letting the uncapped upper mode become the p90.
620
+ if center <= 0 or (mad > 0 and (mad / center) > 0.25):
621
+ return []
622
+ tolerance = max(0.05, abs(center) * 0.10) if mad == 0 else max(0.05, mad * 5.2)
623
+ coherent = [value for value in ratios if abs(value - center) <= tolerance]
624
+ # A valid cluster must contain at least three calls and a strict majority;
625
+ # equal-sized modes remain unstable until one shape dominates.
626
+ return coherent if len(coherent) >= 3 and len(coherent) * 2 > len(ratios) else []
627
+
628
+
629
+ def _resolve_total_tokens(
630
+ *,
631
+ api_total_tokens: int | None,
632
+ prompt_tokens: int,
633
+ completion_tokens: int,
634
+ prompt_character_count: int,
635
+ completion_character_count: int,
636
+ ) -> tuple[int, str | None]:
637
+ expected_total = prompt_tokens + completion_tokens
638
+ if api_total_tokens is None:
639
+ return expected_total, None
640
+ if api_total_tokens < expected_total:
641
+ return expected_total, "api_usage_total_below_component_sum"
642
+ if _looks_like_character_count(
643
+ api_count=api_total_tokens,
644
+ estimated_tokens=expected_total,
645
+ character_count=prompt_character_count + completion_character_count,
646
+ ):
647
+ return expected_total, "api_usage_looked_like_character_counts:total_tokens"
648
+ return api_total_tokens, None
649
+
650
+
651
+ def _resolve_model_rate(
652
+ requested_meta: ModelMeta,
653
+ response_meta: ModelMeta | None,
654
+ field: str,
655
+ ) -> float | None:
656
+ value = getattr(requested_meta, field, None)
657
+ if value is None and response_meta is not None:
658
+ value = getattr(response_meta, field, None)
659
+ return parse_rate(value)
660
+
661
+
662
+ def parse_rate(value: Any) -> float | None:
663
+ try:
664
+ parsed = float(value)
665
+ except (TypeError, ValueError):
666
+ return None
667
+ if parsed < 0:
668
+ return None
669
+ return parsed
670
+
671
+
672
+ @dataclass(frozen=True)
673
+ class _CostComputation:
674
+ cost_usd: float | None
675
+ cache_cost_pricing_missing: bool
676
+ cost_source: CostSource = CostSource.UNKNOWN
677
+
678
+
679
+ def _usage_cost_usd(
680
+ *,
681
+ prompt_tokens: int,
682
+ completion_tokens: int,
683
+ input_tokens_uncached: int | None,
684
+ cache_read_input_tokens: int | None,
685
+ cache_creation_input_tokens: int | None,
686
+ cache_creation_5m_input_tokens: int | None,
687
+ cache_creation_1h_input_tokens: int | None,
688
+ reasoning_tokens: int | None,
689
+ input_rate: float | None,
690
+ output_rate: float | None,
691
+ cache_read_input_rate: float | None,
692
+ cache_creation_input_rate: float | None,
693
+ cache_creation_5m_input_rate: float | None,
694
+ cache_creation_1h_input_rate: float | None,
695
+ reasoning_output_rate: float | None,
696
+ ) -> _CostComputation:
697
+ if input_rate is None or output_rate is None:
698
+ return _CostComputation(cost_usd=None, cache_cost_pricing_missing=False)
699
+
700
+ read_tokens = max(0, cache_read_input_tokens or 0)
701
+ creation_5m_tokens = max(0, cache_creation_5m_input_tokens or 0)
702
+ creation_1h_tokens = max(0, cache_creation_1h_input_tokens or 0)
703
+ creation_total_tokens = max(0, cache_creation_input_tokens or 0)
704
+ creation_ttl_tokens = creation_5m_tokens + creation_1h_tokens
705
+ creation_generic_tokens = max(0, creation_total_tokens - creation_ttl_tokens)
706
+ if creation_total_tokens == 0:
707
+ creation_generic_tokens = 0
708
+
709
+ cache_pricing_missing = False
710
+ cost = 0.0
711
+ uncached_tokens = input_tokens_uncached
712
+ if uncached_tokens is None:
713
+ uncached_tokens = max(0, prompt_tokens - read_tokens - creation_total_tokens)
714
+ cost += max(0, uncached_tokens) * input_rate
715
+
716
+ if read_tokens > 0:
717
+ if cache_read_input_rate is None:
718
+ cache_pricing_missing = True
719
+ else:
720
+ cost += read_tokens * cache_read_input_rate
721
+
722
+ if creation_5m_tokens > 0:
723
+ rate = cache_creation_5m_input_rate or cache_creation_input_rate
724
+ if rate is None:
725
+ cache_pricing_missing = True
726
+ else:
727
+ cost += creation_5m_tokens * rate
728
+
729
+ if creation_1h_tokens > 0:
730
+ rate = cache_creation_1h_input_rate or cache_creation_input_rate
731
+ if rate is None:
732
+ cache_pricing_missing = True
733
+ else:
734
+ cost += creation_1h_tokens * rate
735
+
736
+ if creation_generic_tokens > 0:
737
+ if cache_creation_input_rate is None:
738
+ cache_pricing_missing = True
739
+ else:
740
+ cost += creation_generic_tokens * cache_creation_input_rate
741
+
742
+ billed_completion_tokens = max(0, completion_tokens)
743
+ reasoning = max(0, reasoning_tokens or 0)
744
+ if reasoning > 0 and reasoning_output_rate is not None:
745
+ visible_completion_tokens = max(0, billed_completion_tokens - reasoning)
746
+ cost += visible_completion_tokens * output_rate
747
+ cost += reasoning * reasoning_output_rate
748
+ else:
749
+ cost += billed_completion_tokens * output_rate
750
+
751
+ if cache_pricing_missing:
752
+ return _CostComputation(cost_usd=None, cache_cost_pricing_missing=True)
753
+ return _CostComputation(
754
+ cost_usd=cost,
755
+ cache_cost_pricing_missing=False,
756
+ cost_source=CostSource.CATALOG_ESTIMATE,
757
+ )
758
+
759
+
760
+ def build_usage_record(
761
+ *,
762
+ role: str,
763
+ requested_model: str,
764
+ response_model: str | None,
765
+ messages: list[dict[str, Any]],
766
+ response_content: str,
767
+ response_tool_calls: list[Any],
768
+ api_prompt_tokens: int | None,
769
+ api_completion_tokens: int | None,
770
+ api_total_tokens: int | None,
771
+ registry: ModelRegistry,
772
+ api_usage: Any | None = None,
773
+ api_cached_prompt_tokens: int | None = None,
774
+ api_input_tokens_uncached: int | None = None,
775
+ api_input_tokens_uncached_derived: bool | None = None,
776
+ api_cache_read_input_tokens: int | None = None,
777
+ api_cache_creation_input_tokens: int | None = None,
778
+ api_cache_creation_5m_input_tokens: int | None = None,
779
+ api_cache_creation_1h_input_tokens: int | None = None,
780
+ api_reasoning_tokens: int | None = None,
781
+ api_provider_cost_usd: float | None = None,
782
+ api_raw_provider_usage: dict[str, Any] | None = None,
783
+ tool_list: list[dict[str, Any]] | None = None,
784
+ pinned_prefix_len: int = 0,
785
+ provider_key: str | None = None,
786
+ protocol: str | None = None,
787
+ base_url_host: str | None = None,
788
+ operation: str | None = None,
789
+ request_mode: str | None = None,
790
+ cache_strategy: str | None = None,
791
+ request_plan: Mapping[str, Any] | None = None,
792
+ api_usage_counts_authoritative: bool = False,
793
+ api_prompt_tokens_authoritative: bool | None = None,
794
+ api_usage_confidence: str = UsageConfidence.REPORTED.value,
795
+ api_usage_source_detail: str = UsageSource.PROVIDER_RESPONSE.value,
796
+ api_output_includes_reasoning: bool = True,
797
+ billing_mode: str = BillingMode.METERED_API.value,
798
+ ) -> UsageRecord:
799
+ prompt_tokens_authoritative = (
800
+ bool(api_usage_counts_authoritative)
801
+ if api_prompt_tokens_authoritative is None
802
+ else bool(api_prompt_tokens_authoritative)
803
+ )
804
+ request_token_estimate = estimate_request_token_breakdown(
805
+ messages=messages,
806
+ tool_list=tool_list,
807
+ pinned_prefix_len=pinned_prefix_len,
808
+ )
809
+ estimated_prompt_tokens = request_token_estimate.total_tokens or estimate_prompt_tokens(
810
+ messages
811
+ )
812
+ safe_request_plan = _safe_request_plan_payload(request_plan)
813
+ provider_prompt_estimate_tokens = (
814
+ _safe_int(safe_request_plan.get("serialized_request_estimate_tokens"))
815
+ if safe_request_plan is not None
816
+ else None
817
+ )
818
+ fallback_prompt_estimate_tokens = (
819
+ provider_prompt_estimate_tokens
820
+ if provider_prompt_estimate_tokens is not None
821
+ else estimated_prompt_tokens
822
+ )
823
+ estimated_completion_tokens = estimate_completion_tokens(response_content, response_tool_calls)
824
+ prompt_character_count = _prompt_character_count(messages, tool_list)
825
+ completion_character_count = len(
826
+ _completion_payload_for_estimation(response_content, response_tool_calls)
827
+ )
828
+ if api_usage is not None:
829
+ reported_output_includes_reasoning = bool(
830
+ getattr(api_usage, "output_includes_reasoning", True)
831
+ )
832
+ reported_total_includes_reasoning = bool(
833
+ getattr(api_usage, "total_includes_reasoning", True)
834
+ )
835
+ normalizer = getattr(api_usage, "normalized", None)
836
+ if callable(normalizer):
837
+ api_usage = normalizer()
838
+ api_output_includes_reasoning = bool(
839
+ getattr(api_usage, "output_includes_reasoning", True)
840
+ )
841
+ if not reported_output_includes_reasoning:
842
+ api_completion_tokens = getattr(api_usage, "completion_tokens", None)
843
+ if not reported_total_includes_reasoning:
844
+ api_total_tokens = getattr(api_usage, "total_tokens", None)
845
+ if api_cached_prompt_tokens is None:
846
+ api_cached_prompt_tokens = getattr(api_usage, "cached_prompt_tokens", None)
847
+ if api_input_tokens_uncached is None:
848
+ api_input_tokens_uncached = getattr(api_usage, "input_tokens_uncached", None)
849
+ if api_input_tokens_uncached_derived is None:
850
+ api_input_tokens_uncached_derived = bool(
851
+ getattr(api_usage, "input_tokens_uncached_derived", False)
852
+ )
853
+ if api_cache_read_input_tokens is None:
854
+ api_cache_read_input_tokens = getattr(api_usage, "cache_read_input_tokens", None)
855
+ if api_cache_creation_input_tokens is None:
856
+ api_cache_creation_input_tokens = getattr(
857
+ api_usage,
858
+ "cache_creation_input_tokens",
859
+ None,
860
+ )
861
+ if api_cache_creation_5m_input_tokens is None:
862
+ api_cache_creation_5m_input_tokens = getattr(
863
+ api_usage,
864
+ "cache_creation_5m_input_tokens",
865
+ None,
866
+ )
867
+ if api_cache_creation_1h_input_tokens is None:
868
+ api_cache_creation_1h_input_tokens = getattr(
869
+ api_usage,
870
+ "cache_creation_1h_input_tokens",
871
+ None,
872
+ )
873
+ if api_reasoning_tokens is None:
874
+ api_reasoning_tokens = getattr(api_usage, "reasoning_tokens", None)
875
+ if api_provider_cost_usd is None:
876
+ api_provider_cost_usd = getattr(api_usage, "provider_cost_usd", None)
877
+ if api_raw_provider_usage is None:
878
+ api_raw_provider_usage = getattr(api_usage, "raw_provider_usage", None)
879
+
880
+ raw_api_prompt_tokens = _safe_int(api_prompt_tokens)
881
+ raw_api_completion_tokens = _safe_int(api_completion_tokens)
882
+ raw_api_total_tokens = _safe_int(api_total_tokens)
883
+ prompt_tokens = raw_api_prompt_tokens
884
+ completion_tokens = raw_api_completion_tokens
885
+ total_tokens = raw_api_total_tokens
886
+ prompt_measurement_source = str(api_usage_source_detail or "").strip()
887
+ prompt_measurement_is_local = prompt_measurement_source == UsageSource.LOCAL_ESTIMATE.value
888
+ any_provider_usage_reported = any(
889
+ value is not None for value in (raw_api_completion_tokens, raw_api_total_tokens)
890
+ ) or (raw_api_prompt_tokens is not None and not prompt_measurement_is_local)
891
+ usage_source = "estimate" if prompt_measurement_is_local else "api"
892
+ corrected_fields: list[str] = []
893
+ # Reasoning models (OpenAI o-series / gpt-5, etc.) report output_tokens that
894
+ # INCLUDE hidden reasoning tokens, so the API completion count legitimately
895
+ # exceeds the visible response text. The character-count heuristic compares
896
+ # the API count against a visible-only estimate, so on a concise answer with
897
+ # heavy reasoning it false-positives and would overwrite a correct API count
898
+ # with a far-too-low estimate. Authoritative protocols bypass all such
899
+ # corrections; compatible providers also bypass the completion correction
900
+ # whenever they explicitly report reasoning tokens.
901
+ api_reasoning_present = (_safe_int(api_reasoning_tokens) or 0) > 0
902
+
903
+ if prompt_tokens is None:
904
+ prompt_tokens = fallback_prompt_estimate_tokens
905
+ usage_source = "estimate"
906
+ elif (
907
+ prompt_measurement_source == UsageSource.PROVIDER_RESPONSE.value
908
+ and not api_usage_counts_authoritative
909
+ and _looks_like_character_count(
910
+ api_count=prompt_tokens,
911
+ estimated_tokens=fallback_prompt_estimate_tokens,
912
+ character_count=prompt_character_count,
913
+ )
914
+ ):
915
+ prompt_tokens = fallback_prompt_estimate_tokens
916
+ usage_source = "estimate"
917
+ corrected_fields.append("prompt_tokens")
918
+ if completion_tokens is None:
919
+ completion_tokens = estimated_completion_tokens
920
+ usage_source = "estimate"
921
+ elif (
922
+ not api_usage_counts_authoritative
923
+ and not api_reasoning_present
924
+ and _looks_like_character_count(
925
+ api_count=completion_tokens,
926
+ estimated_tokens=estimated_completion_tokens,
927
+ character_count=completion_character_count,
928
+ )
929
+ ):
930
+ completion_tokens = estimated_completion_tokens
931
+ usage_source = "estimate"
932
+ corrected_fields.append("completion_tokens")
933
+ total_correction_reason: str | None = None
934
+ if total_tokens is None or usage_source == "estimate":
935
+ total_tokens = prompt_tokens + completion_tokens
936
+ if usage_source != "estimate":
937
+ usage_source = "api"
938
+ elif api_usage_counts_authoritative:
939
+ # Native/provider-owned protocols define these counts as part of their
940
+ # response contract. Preserve them exactly, including non-visible
941
+ # reasoning, formatting, and tool-structure tokens.
942
+ total_correction_reason = None
943
+ else:
944
+ total_tokens, total_correction_reason = _resolve_total_tokens(
945
+ api_total_tokens=total_tokens,
946
+ prompt_tokens=prompt_tokens,
947
+ completion_tokens=completion_tokens,
948
+ prompt_character_count=prompt_character_count,
949
+ completion_character_count=completion_character_count,
950
+ )
951
+ if total_correction_reason is not None:
952
+ corrected_fields.append("total_tokens")
953
+ if usage_source == "estimate":
954
+ usage_source_detail = (
955
+ UsageSource.MIXED.value
956
+ if any_provider_usage_reported
957
+ else UsageSource.LOCAL_ESTIMATE.value
958
+ )
959
+ usage_confidence = UsageConfidence.ESTIMATED.value
960
+ else:
961
+ usage_source_detail = (
962
+ str(api_usage_source_detail or "").strip() or UsageSource.PROVIDER_RESPONSE.value
963
+ )
964
+ requested_confidence = str(api_usage_confidence or "").strip().lower()
965
+ if api_usage_counts_authoritative and prompt_tokens_authoritative:
966
+ usage_confidence = UsageConfidence.AUTHORITATIVE.value
967
+ elif requested_confidence in {item.value for item in UsageConfidence}:
968
+ usage_confidence = requested_confidence
969
+ else:
970
+ usage_confidence = UsageConfidence.REPORTED.value
971
+ cache_read_input_tokens = _safe_int(api_cache_read_input_tokens)
972
+ if cache_read_input_tokens is None:
973
+ cache_read_input_tokens = _safe_int(api_cached_prompt_tokens)
974
+ cache_creation_5m_input_tokens = _safe_int(api_cache_creation_5m_input_tokens)
975
+ cache_creation_1h_input_tokens = _safe_int(api_cache_creation_1h_input_tokens)
976
+ cache_creation_input_tokens = _safe_int(api_cache_creation_input_tokens)
977
+ if cache_creation_input_tokens is None:
978
+ creation_parts = [
979
+ value
980
+ for value in (cache_creation_5m_input_tokens, cache_creation_1h_input_tokens)
981
+ if value is not None
982
+ ]
983
+ if creation_parts:
984
+ cache_creation_input_tokens = sum(creation_parts)
985
+ input_tokens_uncached = _safe_int(api_input_tokens_uncached)
986
+ input_tokens_uncached_derived = bool(api_input_tokens_uncached_derived)
987
+ reasoning_tokens = _safe_int(api_reasoning_tokens)
988
+ raw_provider_usage = (
989
+ copy.deepcopy(api_raw_provider_usage) if isinstance(api_raw_provider_usage, dict) else None
990
+ )
991
+
992
+ if "prompt_tokens" in corrected_fields:
993
+ cache_read_input_tokens = None
994
+ cache_creation_input_tokens = None
995
+ cache_creation_5m_input_tokens = None
996
+ cache_creation_1h_input_tokens = None
997
+ input_tokens_uncached = None
998
+ input_tokens_uncached_derived = False
999
+
1000
+ cached_prompt_tokens = cache_read_input_tokens
1001
+ uncached_prompt_tokens: int | None = None
1002
+ if prompt_tokens is not None:
1003
+ if cached_prompt_tokens is not None:
1004
+ uncached_prompt_tokens = max(0, prompt_tokens - cached_prompt_tokens)
1005
+ elif input_tokens_uncached is not None:
1006
+ uncached_prompt_tokens = input_tokens_uncached
1007
+ if input_tokens_uncached is None and prompt_tokens is not None:
1008
+ cached_read = cache_read_input_tokens or 0
1009
+ cache_creation = cache_creation_input_tokens or 0
1010
+ if cached_read > 0 or cache_creation > 0:
1011
+ input_tokens_uncached = max(0, prompt_tokens - cached_read - cache_creation)
1012
+ input_tokens_uncached_derived = True
1013
+ prompt_estimate_error_tokens, prompt_estimate_error_ratio = _prompt_estimate_calibration(
1014
+ api_prompt_tokens=(None if prompt_measurement_is_local else raw_api_prompt_tokens),
1015
+ estimated_prompt_tokens=fallback_prompt_estimate_tokens,
1016
+ corrected_fields=corrected_fields,
1017
+ usage_source=usage_source,
1018
+ api_prompt_tokens_authoritative=prompt_tokens_authoritative,
1019
+ )
1020
+ correction_reasons: list[str] = []
1021
+ character_count_corrected_fields = [
1022
+ field for field in corrected_fields if field in {"prompt_tokens", "completion_tokens"}
1023
+ ]
1024
+ if character_count_corrected_fields:
1025
+ correction_reasons.append(
1026
+ "api_usage_looked_like_character_counts:" + ",".join(character_count_corrected_fields)
1027
+ )
1028
+ if total_correction_reason is not None:
1029
+ correction_reasons.append(total_correction_reason)
1030
+ usage_correction_reason = ";".join(correction_reasons) if correction_reasons else None
1031
+ requested_meta = registry.get(requested_model)
1032
+ response_meta: ModelMeta | None = None
1033
+ if response_model:
1034
+ response_meta = registry.get(response_model)
1035
+
1036
+ input_rate = requested_meta.input_cost_per_token
1037
+ output_rate = requested_meta.output_cost_per_token
1038
+ if input_rate is None and response_meta is not None:
1039
+ input_rate = response_meta.input_cost_per_token
1040
+ if output_rate is None and response_meta is not None:
1041
+ output_rate = response_meta.output_cost_per_token
1042
+ cache_read_input_rate = _resolve_model_rate(
1043
+ requested_meta,
1044
+ response_meta,
1045
+ "cache_read_input_cost_per_token",
1046
+ )
1047
+ cache_creation_input_rate = _resolve_model_rate(
1048
+ requested_meta,
1049
+ response_meta,
1050
+ "cache_creation_input_cost_per_token",
1051
+ )
1052
+ cache_creation_5m_input_rate = _resolve_model_rate(
1053
+ requested_meta,
1054
+ response_meta,
1055
+ "cache_creation_5m_input_cost_per_token",
1056
+ )
1057
+ cache_creation_1h_input_rate = _resolve_model_rate(
1058
+ requested_meta,
1059
+ response_meta,
1060
+ "cache_creation_1h_input_cost_per_token",
1061
+ )
1062
+ reasoning_output_rate = _resolve_model_rate(
1063
+ requested_meta,
1064
+ response_meta,
1065
+ "reasoning_output_cost_per_token",
1066
+ )
1067
+ safe_billing_mode = str(billing_mode or "").strip().lower()
1068
+ if safe_billing_mode not in {item.value for item in BillingMode}:
1069
+ safe_billing_mode = BillingMode.UNKNOWN.value
1070
+ provider_cost_usd = _safe_float(api_provider_cost_usd)
1071
+ if safe_billing_mode == BillingMode.SUBSCRIPTION.value:
1072
+ cost = _CostComputation(None, False, CostSource.SUBSCRIPTION)
1073
+ elif safe_billing_mode == BillingMode.INCLUDED.value:
1074
+ cost = _CostComputation(None, False, CostSource.INCLUDED)
1075
+ elif safe_billing_mode == BillingMode.LOCAL.value:
1076
+ cost = _CostComputation(None, False, CostSource.LOCAL)
1077
+ elif provider_cost_usd is not None and provider_cost_usd >= 0:
1078
+ cost = _CostComputation(
1079
+ cost_usd=provider_cost_usd,
1080
+ cache_cost_pricing_missing=False,
1081
+ cost_source=CostSource.PROVIDER_REPORTED,
1082
+ )
1083
+ else:
1084
+ cost = _usage_cost_usd(
1085
+ prompt_tokens=prompt_tokens,
1086
+ completion_tokens=completion_tokens,
1087
+ input_tokens_uncached=input_tokens_uncached,
1088
+ cache_read_input_tokens=cache_read_input_tokens,
1089
+ cache_creation_input_tokens=cache_creation_input_tokens,
1090
+ cache_creation_5m_input_tokens=cache_creation_5m_input_tokens,
1091
+ cache_creation_1h_input_tokens=cache_creation_1h_input_tokens,
1092
+ reasoning_tokens=reasoning_tokens,
1093
+ input_rate=input_rate,
1094
+ output_rate=output_rate,
1095
+ cache_read_input_rate=cache_read_input_rate,
1096
+ cache_creation_input_rate=cache_creation_input_rate,
1097
+ cache_creation_5m_input_rate=cache_creation_5m_input_rate,
1098
+ cache_creation_1h_input_rate=cache_creation_1h_input_rate,
1099
+ reasoning_output_rate=reasoning_output_rate,
1100
+ )
1101
+ safe_request_mode = _safe_label(request_mode) or (
1102
+ str(safe_request_plan.get("input_mode"))
1103
+ if safe_request_plan and safe_request_plan.get("input_mode")
1104
+ else None
1105
+ )
1106
+ safe_cache_strategy = _safe_label(cache_strategy) or (
1107
+ str(safe_request_plan.get("cache_strategy"))
1108
+ if safe_request_plan and safe_request_plan.get("cache_strategy")
1109
+ else None
1110
+ )
1111
+
1112
+ return UsageRecord(
1113
+ usage_schema_version=6,
1114
+ timestamp=now_iso(),
1115
+ role=role,
1116
+ requested_model=requested_model,
1117
+ response_model=response_model,
1118
+ prompt_tokens=prompt_tokens,
1119
+ completion_tokens=completion_tokens,
1120
+ total_tokens=total_tokens,
1121
+ input_cost_per_token=input_rate,
1122
+ output_cost_per_token=output_rate,
1123
+ cache_read_input_cost_per_token=cache_read_input_rate,
1124
+ cache_creation_input_cost_per_token=cache_creation_input_rate,
1125
+ cache_creation_5m_input_cost_per_token=cache_creation_5m_input_rate,
1126
+ cache_creation_1h_input_cost_per_token=cache_creation_1h_input_rate,
1127
+ reasoning_output_cost_per_token=reasoning_output_rate,
1128
+ cost_usd=cost.cost_usd,
1129
+ billing_mode=safe_billing_mode,
1130
+ cost_source=cost.cost_source.value,
1131
+ provider_cost_usd=provider_cost_usd,
1132
+ usage_source=usage_source,
1133
+ usage_source_detail=usage_source_detail,
1134
+ usage_confidence=usage_confidence,
1135
+ output_includes_reasoning=bool(api_output_includes_reasoning),
1136
+ provider_key=_safe_label(provider_key),
1137
+ protocol=_safe_label(protocol),
1138
+ base_url_host=_safe_label(base_url_host),
1139
+ operation=_safe_label(operation),
1140
+ request_mode=safe_request_mode,
1141
+ cache_strategy=safe_cache_strategy,
1142
+ request_plan=safe_request_plan,
1143
+ request_has_media=request_contains_media(messages),
1144
+ cached_prompt_tokens=cached_prompt_tokens,
1145
+ uncached_prompt_tokens=uncached_prompt_tokens,
1146
+ input_tokens_uncached=input_tokens_uncached,
1147
+ input_tokens_uncached_derived=input_tokens_uncached_derived,
1148
+ cache_read_input_tokens=cache_read_input_tokens,
1149
+ cache_creation_input_tokens=cache_creation_input_tokens,
1150
+ cache_creation_5m_input_tokens=cache_creation_5m_input_tokens,
1151
+ cache_creation_1h_input_tokens=cache_creation_1h_input_tokens,
1152
+ reasoning_tokens=reasoning_tokens,
1153
+ raw_provider_usage=raw_provider_usage,
1154
+ cache_cost_pricing_missing=cost.cache_cost_pricing_missing,
1155
+ request_token_estimate=request_token_estimate,
1156
+ prompt_estimate_tokens=fallback_prompt_estimate_tokens,
1157
+ prompt_estimate_error_tokens=prompt_estimate_error_tokens,
1158
+ prompt_estimate_error_ratio=prompt_estimate_error_ratio,
1159
+ raw_api_prompt_tokens=raw_api_prompt_tokens if corrected_fields else None,
1160
+ raw_api_completion_tokens=raw_api_completion_tokens if corrected_fields else None,
1161
+ raw_api_total_tokens=raw_api_total_tokens if corrected_fields else None,
1162
+ usage_correction_reason=usage_correction_reason,
1163
+ )
1164
+
1165
+
1166
+ class UsageSummary:
1167
+ def __init__(self) -> None:
1168
+ self._lock = threading.RLock()
1169
+ self._by_model: dict[str, _ModelUsageTotals] = {}
1170
+ self._records: list[UsageRecord] = []
1171
+ self.calls = 0
1172
+
1173
+ def add_record(self, record: UsageRecord) -> None:
1174
+ with self._lock:
1175
+ key = record.requested_model.strip() or "unknown-model"
1176
+ totals = self._by_model.setdefault(key, _ModelUsageTotals())
1177
+ self._records.append(record)
1178
+ totals.prompt_tokens += record.prompt_tokens
1179
+ totals.completion_tokens += record.completion_tokens
1180
+ totals.total_tokens += record.total_tokens
1181
+ if record.cost_usd is None:
1182
+ if record.cost_source == CostSource.SUBSCRIPTION.value:
1183
+ totals.subscription_calls += 1
1184
+ elif record.cost_source == CostSource.INCLUDED.value:
1185
+ totals.included_calls += 1
1186
+ elif record.cost_source == CostSource.LOCAL.value:
1187
+ totals.local_calls += 1
1188
+ else:
1189
+ totals.unknown_cost_count += 1
1190
+ else:
1191
+ totals.known_cost_usd += float(record.cost_usd)
1192
+ totals.known_cost_calls += 1
1193
+ if record.cost_source == CostSource.PROVIDER_REPORTED.value:
1194
+ totals.provider_reported_cost_calls += 1
1195
+ elif record.cost_source == CostSource.CATALOG_ESTIMATE.value:
1196
+ totals.catalog_estimated_cost_calls += 1
1197
+ if record.usage_source == "api":
1198
+ totals.api_usage_calls += 1
1199
+ else:
1200
+ totals.estimate_usage_calls += 1
1201
+ if record.usage_confidence == UsageConfidence.AUTHORITATIVE.value:
1202
+ totals.authoritative_usage_calls += 1
1203
+ elif record.usage_confidence == UsageConfidence.REPORTED.value:
1204
+ totals.reported_usage_calls += 1
1205
+ if record.usage_correction_reason:
1206
+ totals.corrected_usage_calls += 1
1207
+ if record.prompt_estimate_error_ratio is not None:
1208
+ totals.prompt_estimate_calibration_calls += 1
1209
+ totals.prompt_estimate_error_ratio_sum += float(record.prompt_estimate_error_ratio)
1210
+ totals.prompt_estimate_error_ratio_max = max(
1211
+ totals.prompt_estimate_error_ratio_max,
1212
+ float(record.prompt_estimate_error_ratio),
1213
+ )
1214
+ error_tokens = record.prompt_estimate_error_tokens or 0
1215
+ if error_tokens > 0:
1216
+ totals.prompt_estimate_underestimate_calls += 1
1217
+ elif error_tokens < 0:
1218
+ totals.prompt_estimate_overestimate_calls += 1
1219
+ totals.cached_prompt_tokens += max(0, record.cached_prompt_tokens or 0)
1220
+ totals.uncached_prompt_tokens += max(0, record.uncached_prompt_tokens or 0)
1221
+ totals.input_tokens_uncached += max(0, record.input_tokens_uncached or 0)
1222
+ totals.cache_read_input_tokens += max(0, record.cache_read_input_tokens or 0)
1223
+ totals.cache_creation_input_tokens += max(0, record.cache_creation_input_tokens or 0)
1224
+ if record.input_tokens_uncached is not None:
1225
+ if record.input_tokens_uncached_derived:
1226
+ totals.input_tokens_uncached_derived_calls += 1
1227
+ else:
1228
+ totals.input_tokens_uncached_reported_calls += 1
1229
+ if record.cache_read_input_tokens is not None:
1230
+ totals.cache_read_reported_calls += 1
1231
+ if record.cache_creation_input_tokens is not None:
1232
+ totals.cache_creation_reported_calls += 1
1233
+ totals.cache_creation_5m_input_tokens += max(
1234
+ 0,
1235
+ record.cache_creation_5m_input_tokens or 0,
1236
+ )
1237
+ totals.cache_creation_1h_input_tokens += max(
1238
+ 0,
1239
+ record.cache_creation_1h_input_tokens or 0,
1240
+ )
1241
+ totals.reasoning_tokens += max(0, record.reasoning_tokens or 0)
1242
+ if record.cache_cost_pricing_missing:
1243
+ totals.cache_cost_pricing_missing_calls += 1
1244
+ breakdown = record.request_token_estimate
1245
+ if breakdown is not None:
1246
+ totals.estimated_bootstrap_prompt_tokens += breakdown.bootstrap_prompt_tokens
1247
+ totals.estimated_tool_schema_tokens += breakdown.tool_schema_tokens
1248
+ totals.estimated_live_conversation_history_tokens += (
1249
+ breakdown.live_conversation_history_tokens
1250
+ )
1251
+ totals.estimated_inline_tool_transcript_tokens += (
1252
+ breakdown.inline_tool_transcript_tokens
1253
+ )
1254
+ totals.estimated_memory_summary_tokens += breakdown.memory_summary_tokens
1255
+ totals.estimated_pins_tokens += breakdown.pins_tokens
1256
+ totals.estimated_total_request_tokens += breakdown.total_tokens
1257
+ tool_budget = breakdown.tool_schema_budget
1258
+ if tool_budget is not None:
1259
+ totals.tool_schema_budget_reported_calls += 1
1260
+ totals.tool_schema_budget_overage_tokens += tool_budget.over_budget_tokens
1261
+ if tool_budget.over_budget:
1262
+ totals.tool_schema_budget_exceeded_calls += 1
1263
+ if tool_budget.largest_tools:
1264
+ totals.tool_schema_largest_tool_tokens = max(
1265
+ totals.tool_schema_largest_tool_tokens,
1266
+ tool_budget.largest_tools[0].token_estimate,
1267
+ )
1268
+ self.calls += 1
1269
+
1270
+ def records(self) -> list[UsageRecord]:
1271
+ with self._lock:
1272
+ return list(self._records)
1273
+
1274
+ def cache_efficiency_summary(self) -> dict[str, Any]:
1275
+ with self._lock:
1276
+ reported = [
1277
+ (position, record)
1278
+ for position, record in enumerate(self._records, start=1)
1279
+ if record.cached_prompt_tokens is not None
1280
+ or record.uncached_prompt_tokens is not None
1281
+ ]
1282
+ cached = sum(max(0, record.cached_prompt_tokens or 0) for _, record in reported)
1283
+ uncached = sum(max(0, record.uncached_prompt_tokens or 0) for _, record in reported)
1284
+ denominator = cached + uncached
1285
+ largest = max(
1286
+ reported,
1287
+ key=lambda item: max(0, item[1].uncached_prompt_tokens or 0),
1288
+ default=None,
1289
+ )
1290
+ largest_payload: dict[str, Any] | None = None
1291
+ if largest is not None:
1292
+ position, record = largest
1293
+ largest_payload = {
1294
+ "call_position": position,
1295
+ "uncached_prompt_tokens": max(
1296
+ 0,
1297
+ record.uncached_prompt_tokens or 0,
1298
+ ),
1299
+ "role": record.role,
1300
+ "requested_model": record.requested_model,
1301
+ "timestamp": record.timestamp,
1302
+ }
1303
+ return {
1304
+ "reported_calls": len(reported),
1305
+ "cached_prompt_tokens": cached,
1306
+ "uncached_prompt_tokens": uncached,
1307
+ "hit_ratio": cached / denominator if denominator > 0 else None,
1308
+ "largest_uncached_call": largest_payload,
1309
+ }
1310
+
1311
+ def merge_records(self, records: Iterable[UsageRecord]) -> int:
1312
+ merged = 0
1313
+ for record in records:
1314
+ self.add_record(record)
1315
+ merged += 1
1316
+ return merged
1317
+
1318
+ def add_event_payload(self, payload: dict[str, Any]) -> None:
1319
+ if str(payload.get("event_type") or "") != "llm_usage":
1320
+ return
1321
+ record = UsageRecord(
1322
+ usage_schema_version=max(1, _safe_int(payload.get("usage_schema_version")) or 1),
1323
+ timestamp=str(payload.get("timestamp") or ""),
1324
+ role=str(payload.get("role") or "main"),
1325
+ requested_model=str(payload.get("requested_model") or "unknown-model"),
1326
+ response_model=(
1327
+ str(payload.get("response_model"))
1328
+ if payload.get("response_model") is not None
1329
+ else None
1330
+ ),
1331
+ prompt_tokens=max(0, _safe_int(payload.get("prompt_tokens")) or 0),
1332
+ completion_tokens=max(0, _safe_int(payload.get("completion_tokens")) or 0),
1333
+ total_tokens=max(0, _safe_int(payload.get("total_tokens")) or 0),
1334
+ input_cost_per_token=(
1335
+ _safe_float(payload.get("input_cost_per_token"))
1336
+ if payload.get("input_cost_per_token") is not None
1337
+ else None
1338
+ ),
1339
+ output_cost_per_token=(
1340
+ _safe_float(payload.get("output_cost_per_token"))
1341
+ if payload.get("output_cost_per_token") is not None
1342
+ else None
1343
+ ),
1344
+ cache_read_input_cost_per_token=(
1345
+ _safe_float(payload.get("cache_read_input_cost_per_token"))
1346
+ if payload.get("cache_read_input_cost_per_token") is not None
1347
+ else None
1348
+ ),
1349
+ cache_creation_input_cost_per_token=(
1350
+ _safe_float(payload.get("cache_creation_input_cost_per_token"))
1351
+ if payload.get("cache_creation_input_cost_per_token") is not None
1352
+ else None
1353
+ ),
1354
+ cache_creation_5m_input_cost_per_token=(
1355
+ _safe_float(payload.get("cache_creation_5m_input_cost_per_token"))
1356
+ if payload.get("cache_creation_5m_input_cost_per_token") is not None
1357
+ else None
1358
+ ),
1359
+ cache_creation_1h_input_cost_per_token=(
1360
+ _safe_float(payload.get("cache_creation_1h_input_cost_per_token"))
1361
+ if payload.get("cache_creation_1h_input_cost_per_token") is not None
1362
+ else None
1363
+ ),
1364
+ reasoning_output_cost_per_token=(
1365
+ _safe_float(payload.get("reasoning_output_cost_per_token"))
1366
+ if payload.get("reasoning_output_cost_per_token") is not None
1367
+ else None
1368
+ ),
1369
+ cost_usd=(
1370
+ _safe_float(payload.get("cost_usd"))
1371
+ if payload.get("cost_usd") is not None
1372
+ else None
1373
+ ),
1374
+ billing_mode=str(payload.get("billing_mode") or BillingMode.METERED_API.value),
1375
+ cost_source=str(payload.get("cost_source") or CostSource.UNKNOWN.value),
1376
+ provider_cost_usd=(
1377
+ _safe_float(payload.get("provider_cost_usd"))
1378
+ if payload.get("provider_cost_usd") is not None
1379
+ else None
1380
+ ),
1381
+ usage_source=str(payload.get("usage_source") or "estimate"),
1382
+ usage_source_detail=str(
1383
+ payload.get("usage_source_detail")
1384
+ or (
1385
+ UsageSource.PROVIDER_RESPONSE.value
1386
+ if str(payload.get("usage_source") or "") == "api"
1387
+ else UsageSource.LOCAL_ESTIMATE.value
1388
+ )
1389
+ ),
1390
+ usage_confidence=str(
1391
+ payload.get("usage_confidence")
1392
+ or (
1393
+ UsageConfidence.REPORTED.value
1394
+ if str(payload.get("usage_source") or "") == "api"
1395
+ else UsageConfidence.ESTIMATED.value
1396
+ )
1397
+ ),
1398
+ output_includes_reasoning=_safe_bool(payload.get("output_includes_reasoning", True)),
1399
+ provider_key=_safe_label(payload.get("provider_key")),
1400
+ protocol=_safe_label(payload.get("protocol")),
1401
+ base_url_host=_safe_label(payload.get("base_url_host")),
1402
+ operation=_safe_label(payload.get("operation")),
1403
+ request_mode=_safe_label(payload.get("request_mode")),
1404
+ cache_strategy=_safe_label(payload.get("cache_strategy")),
1405
+ request_plan=_safe_request_plan_payload(payload.get("request_plan")),
1406
+ request_has_media=_safe_bool(payload.get("request_has_media")),
1407
+ cached_prompt_tokens=_safe_int(payload.get("cached_prompt_tokens")),
1408
+ uncached_prompt_tokens=_safe_int(payload.get("uncached_prompt_tokens")),
1409
+ input_tokens_uncached=_safe_int(payload.get("input_tokens_uncached")),
1410
+ input_tokens_uncached_derived=bool(payload.get("input_tokens_uncached_derived")),
1411
+ cache_read_input_tokens=(
1412
+ _safe_int(payload.get("cache_read_input_tokens"))
1413
+ if payload.get("cache_read_input_tokens") is not None
1414
+ else _safe_int(payload.get("cached_prompt_tokens"))
1415
+ ),
1416
+ cache_creation_input_tokens=_safe_int(payload.get("cache_creation_input_tokens")),
1417
+ cache_creation_5m_input_tokens=_safe_int(payload.get("cache_creation_5m_input_tokens")),
1418
+ cache_creation_1h_input_tokens=_safe_int(payload.get("cache_creation_1h_input_tokens")),
1419
+ reasoning_tokens=_safe_int(payload.get("reasoning_tokens")),
1420
+ raw_provider_usage=(
1421
+ copy.deepcopy(payload.get("raw_provider_usage"))
1422
+ if isinstance(payload.get("raw_provider_usage"), dict)
1423
+ else None
1424
+ ),
1425
+ cache_cost_pricing_missing=_safe_bool(payload.get("cache_cost_pricing_missing")),
1426
+ request_token_estimate=RequestTokenBreakdown.from_payload(
1427
+ payload.get("request_token_estimate")
1428
+ ),
1429
+ prompt_estimate_tokens=_safe_int(payload.get("prompt_estimate_tokens")),
1430
+ prompt_estimate_error_tokens=_safe_int(payload.get("prompt_estimate_error_tokens")),
1431
+ prompt_estimate_error_ratio=(
1432
+ _safe_float(payload.get("prompt_estimate_error_ratio"))
1433
+ if payload.get("prompt_estimate_error_ratio") is not None
1434
+ else None
1435
+ ),
1436
+ raw_api_prompt_tokens=_safe_int(payload.get("raw_api_prompt_tokens")),
1437
+ raw_api_completion_tokens=_safe_int(payload.get("raw_api_completion_tokens")),
1438
+ raw_api_total_tokens=_safe_int(payload.get("raw_api_total_tokens")),
1439
+ usage_correction_reason=(
1440
+ str(payload.get("usage_correction_reason"))
1441
+ if payload.get("usage_correction_reason") is not None
1442
+ else None
1443
+ ),
1444
+ )
1445
+ self.add_record(record)
1446
+
1447
+ def by_model_rows(self) -> list[dict[str, Any]]:
1448
+ with self._lock:
1449
+ rows: list[dict[str, Any]] = []
1450
+ for model_name in sorted(self._by_model):
1451
+ totals = self._by_model[model_name]
1452
+ rows.append(
1453
+ {
1454
+ "model": model_name,
1455
+ "prompt_tokens": totals.prompt_tokens,
1456
+ "completion_tokens": totals.completion_tokens,
1457
+ "total_tokens": totals.total_tokens,
1458
+ "cost_usd": (
1459
+ totals.known_cost_usd if totals.known_cost_calls > 0 else None
1460
+ ),
1461
+ "known_cost_calls": totals.known_cost_calls,
1462
+ "unknown_cost_count": totals.unknown_cost_count,
1463
+ "provider_reported_cost_calls": totals.provider_reported_cost_calls,
1464
+ "catalog_estimated_cost_calls": totals.catalog_estimated_cost_calls,
1465
+ "subscription_calls": totals.subscription_calls,
1466
+ "included_calls": totals.included_calls,
1467
+ "local_calls": totals.local_calls,
1468
+ "api_usage_calls": totals.api_usage_calls,
1469
+ "estimate_usage_calls": totals.estimate_usage_calls,
1470
+ "authoritative_usage_calls": totals.authoritative_usage_calls,
1471
+ "reported_usage_calls": totals.reported_usage_calls,
1472
+ "corrected_usage_calls": totals.corrected_usage_calls,
1473
+ "prompt_estimate_calibration_calls": (
1474
+ totals.prompt_estimate_calibration_calls
1475
+ ),
1476
+ "prompt_estimate_error_ratio_avg": (
1477
+ totals.prompt_estimate_error_ratio_sum
1478
+ / totals.prompt_estimate_calibration_calls
1479
+ if totals.prompt_estimate_calibration_calls > 0
1480
+ else None
1481
+ ),
1482
+ "prompt_estimate_error_ratio_max": (
1483
+ totals.prompt_estimate_error_ratio_max
1484
+ if totals.prompt_estimate_calibration_calls > 0
1485
+ else None
1486
+ ),
1487
+ "prompt_estimate_underestimate_calls": (
1488
+ totals.prompt_estimate_underestimate_calls
1489
+ ),
1490
+ "prompt_estimate_overestimate_calls": (
1491
+ totals.prompt_estimate_overestimate_calls
1492
+ ),
1493
+ "cached_prompt_tokens": totals.cached_prompt_tokens,
1494
+ "uncached_prompt_tokens": totals.uncached_prompt_tokens,
1495
+ "input_tokens_uncached": totals.input_tokens_uncached,
1496
+ "cache_read_input_tokens": totals.cache_read_input_tokens,
1497
+ "cache_creation_input_tokens": totals.cache_creation_input_tokens,
1498
+ "cache_creation_5m_input_tokens": (totals.cache_creation_5m_input_tokens),
1499
+ "cache_creation_1h_input_tokens": (totals.cache_creation_1h_input_tokens),
1500
+ "input_tokens_uncached_reported_calls": (
1501
+ totals.input_tokens_uncached_reported_calls
1502
+ ),
1503
+ "input_tokens_uncached_derived_calls": (
1504
+ totals.input_tokens_uncached_derived_calls
1505
+ ),
1506
+ "cache_read_reported_calls": totals.cache_read_reported_calls,
1507
+ "cache_creation_reported_calls": totals.cache_creation_reported_calls,
1508
+ "reasoning_tokens": totals.reasoning_tokens,
1509
+ "cache_cost_pricing_missing_calls": (
1510
+ totals.cache_cost_pricing_missing_calls
1511
+ ),
1512
+ "request_token_estimate": {
1513
+ "bootstrap_prompt_tokens": totals.estimated_bootstrap_prompt_tokens,
1514
+ "tool_schema_tokens": totals.estimated_tool_schema_tokens,
1515
+ "live_conversation_history_tokens": (
1516
+ totals.estimated_live_conversation_history_tokens
1517
+ ),
1518
+ "inline_tool_transcript_tokens": (
1519
+ totals.estimated_inline_tool_transcript_tokens
1520
+ ),
1521
+ "memory_summary_tokens": totals.estimated_memory_summary_tokens,
1522
+ "pins_tokens": totals.estimated_pins_tokens,
1523
+ "total_tokens": totals.estimated_total_request_tokens,
1524
+ "tool_schema_budget_reported_calls": (
1525
+ totals.tool_schema_budget_reported_calls
1526
+ ),
1527
+ "tool_schema_budget_exceeded_calls": (
1528
+ totals.tool_schema_budget_exceeded_calls
1529
+ ),
1530
+ "tool_schema_budget_overage_tokens": (
1531
+ totals.tool_schema_budget_overage_tokens
1532
+ ),
1533
+ "tool_schema_largest_tool_tokens": (
1534
+ totals.tool_schema_largest_tool_tokens
1535
+ ),
1536
+ },
1537
+ }
1538
+ )
1539
+ return rows
1540
+
1541
+ def totals(self) -> dict[str, Any]:
1542
+ with self._lock:
1543
+ prompt = sum(v.prompt_tokens for v in self._by_model.values())
1544
+ completion = sum(v.completion_tokens for v in self._by_model.values())
1545
+ total = sum(v.total_tokens for v in self._by_model.values())
1546
+ known_cost = sum(v.known_cost_usd for v in self._by_model.values())
1547
+ known_cost_calls = sum(v.known_cost_calls for v in self._by_model.values())
1548
+ unknown_cost_calls = sum(v.unknown_cost_count for v in self._by_model.values())
1549
+ provider_reported_cost_calls = sum(
1550
+ v.provider_reported_cost_calls for v in self._by_model.values()
1551
+ )
1552
+ catalog_estimated_cost_calls = sum(
1553
+ v.catalog_estimated_cost_calls for v in self._by_model.values()
1554
+ )
1555
+ subscription_calls = sum(v.subscription_calls for v in self._by_model.values())
1556
+ included_calls = sum(v.included_calls for v in self._by_model.values())
1557
+ local_calls = sum(v.local_calls for v in self._by_model.values())
1558
+ api_usage_calls = sum(v.api_usage_calls for v in self._by_model.values())
1559
+ estimate_usage_calls = sum(v.estimate_usage_calls for v in self._by_model.values())
1560
+ authoritative_usage_calls = sum(
1561
+ v.authoritative_usage_calls for v in self._by_model.values()
1562
+ )
1563
+ reported_usage_calls = sum(v.reported_usage_calls for v in self._by_model.values())
1564
+ corrected_usage_calls = sum(v.corrected_usage_calls for v in self._by_model.values())
1565
+ cached_prompt_tokens = sum(v.cached_prompt_tokens for v in self._by_model.values())
1566
+ uncached_prompt_tokens = sum(v.uncached_prompt_tokens for v in self._by_model.values())
1567
+ input_tokens_uncached = sum(v.input_tokens_uncached for v in self._by_model.values())
1568
+ cache_read_input_tokens = sum(
1569
+ v.cache_read_input_tokens for v in self._by_model.values()
1570
+ )
1571
+ cache_creation_input_tokens = sum(
1572
+ v.cache_creation_input_tokens for v in self._by_model.values()
1573
+ )
1574
+ cache_creation_5m_input_tokens = sum(
1575
+ v.cache_creation_5m_input_tokens for v in self._by_model.values()
1576
+ )
1577
+ cache_creation_1h_input_tokens = sum(
1578
+ v.cache_creation_1h_input_tokens for v in self._by_model.values()
1579
+ )
1580
+ input_tokens_uncached_reported_calls = sum(
1581
+ v.input_tokens_uncached_reported_calls for v in self._by_model.values()
1582
+ )
1583
+ input_tokens_uncached_derived_calls = sum(
1584
+ v.input_tokens_uncached_derived_calls for v in self._by_model.values()
1585
+ )
1586
+ cache_read_reported_calls = sum(
1587
+ v.cache_read_reported_calls for v in self._by_model.values()
1588
+ )
1589
+ cache_creation_reported_calls = sum(
1590
+ v.cache_creation_reported_calls for v in self._by_model.values()
1591
+ )
1592
+ reasoning_tokens = sum(v.reasoning_tokens for v in self._by_model.values())
1593
+ cache_cost_pricing_missing_calls = sum(
1594
+ v.cache_cost_pricing_missing_calls for v in self._by_model.values()
1595
+ )
1596
+ prompt_estimate_calibration_calls = sum(
1597
+ v.prompt_estimate_calibration_calls for v in self._by_model.values()
1598
+ )
1599
+ prompt_estimate_error_ratio_sum = sum(
1600
+ v.prompt_estimate_error_ratio_sum for v in self._by_model.values()
1601
+ )
1602
+ prompt_estimate_error_ratio_max = max(
1603
+ (v.prompt_estimate_error_ratio_max for v in self._by_model.values()),
1604
+ default=0.0,
1605
+ )
1606
+ prompt_estimate_underestimate_calls = sum(
1607
+ v.prompt_estimate_underestimate_calls for v in self._by_model.values()
1608
+ )
1609
+ prompt_estimate_overestimate_calls = sum(
1610
+ v.prompt_estimate_overestimate_calls for v in self._by_model.values()
1611
+ )
1612
+ estimated_bootstrap_prompt_tokens = sum(
1613
+ v.estimated_bootstrap_prompt_tokens for v in self._by_model.values()
1614
+ )
1615
+ estimated_tool_schema_tokens = sum(
1616
+ v.estimated_tool_schema_tokens for v in self._by_model.values()
1617
+ )
1618
+ estimated_live_conversation_history_tokens = sum(
1619
+ v.estimated_live_conversation_history_tokens for v in self._by_model.values()
1620
+ )
1621
+ estimated_inline_tool_transcript_tokens = sum(
1622
+ v.estimated_inline_tool_transcript_tokens for v in self._by_model.values()
1623
+ )
1624
+ estimated_memory_summary_tokens = sum(
1625
+ v.estimated_memory_summary_tokens for v in self._by_model.values()
1626
+ )
1627
+ estimated_pins_tokens = sum(v.estimated_pins_tokens for v in self._by_model.values())
1628
+ estimated_total_request_tokens = sum(
1629
+ v.estimated_total_request_tokens for v in self._by_model.values()
1630
+ )
1631
+ tool_schema_budget_reported_calls = sum(
1632
+ v.tool_schema_budget_reported_calls for v in self._by_model.values()
1633
+ )
1634
+ tool_schema_budget_exceeded_calls = sum(
1635
+ v.tool_schema_budget_exceeded_calls for v in self._by_model.values()
1636
+ )
1637
+ tool_schema_budget_overage_tokens = sum(
1638
+ v.tool_schema_budget_overage_tokens for v in self._by_model.values()
1639
+ )
1640
+ tool_schema_largest_tool_tokens = max(
1641
+ (v.tool_schema_largest_tool_tokens for v in self._by_model.values()),
1642
+ default=0,
1643
+ )
1644
+ return {
1645
+ "prompt_tokens": prompt,
1646
+ "completion_tokens": completion,
1647
+ "total_tokens": total,
1648
+ "cost_usd": known_cost if known_cost_calls > 0 else None,
1649
+ "known_cost_usd": known_cost,
1650
+ "known_cost_calls": known_cost_calls,
1651
+ "unknown_cost_calls": unknown_cost_calls,
1652
+ "provider_reported_cost_calls": provider_reported_cost_calls,
1653
+ "catalog_estimated_cost_calls": catalog_estimated_cost_calls,
1654
+ "subscription_calls": subscription_calls,
1655
+ "included_calls": included_calls,
1656
+ "local_calls": local_calls,
1657
+ "api_usage_calls": api_usage_calls,
1658
+ "estimate_usage_calls": estimate_usage_calls,
1659
+ "authoritative_usage_calls": authoritative_usage_calls,
1660
+ "reported_usage_calls": reported_usage_calls,
1661
+ "corrected_usage_calls": corrected_usage_calls,
1662
+ "prompt_estimate_calibration_calls": prompt_estimate_calibration_calls,
1663
+ "prompt_estimate_error_ratio_avg": (
1664
+ prompt_estimate_error_ratio_sum / prompt_estimate_calibration_calls
1665
+ if prompt_estimate_calibration_calls > 0
1666
+ else None
1667
+ ),
1668
+ "prompt_estimate_error_ratio_max": (
1669
+ prompt_estimate_error_ratio_max
1670
+ if prompt_estimate_calibration_calls > 0
1671
+ else None
1672
+ ),
1673
+ "prompt_estimate_underestimate_calls": prompt_estimate_underestimate_calls,
1674
+ "prompt_estimate_overestimate_calls": prompt_estimate_overestimate_calls,
1675
+ "cached_prompt_tokens": cached_prompt_tokens,
1676
+ "uncached_prompt_tokens": uncached_prompt_tokens,
1677
+ "input_tokens_uncached": input_tokens_uncached,
1678
+ "cache_read_input_tokens": cache_read_input_tokens,
1679
+ "cache_creation_input_tokens": cache_creation_input_tokens,
1680
+ "cache_creation_5m_input_tokens": cache_creation_5m_input_tokens,
1681
+ "cache_creation_1h_input_tokens": cache_creation_1h_input_tokens,
1682
+ "input_tokens_uncached_reported_calls": input_tokens_uncached_reported_calls,
1683
+ "input_tokens_uncached_derived_calls": input_tokens_uncached_derived_calls,
1684
+ "cache_read_reported_calls": cache_read_reported_calls,
1685
+ "cache_creation_reported_calls": cache_creation_reported_calls,
1686
+ "reasoning_tokens": reasoning_tokens,
1687
+ "cache_cost_pricing_missing_calls": cache_cost_pricing_missing_calls,
1688
+ "cache_efficiency": self.cache_efficiency_summary(),
1689
+ "request_token_estimate": {
1690
+ "bootstrap_prompt_tokens": estimated_bootstrap_prompt_tokens,
1691
+ "tool_schema_tokens": estimated_tool_schema_tokens,
1692
+ "live_conversation_history_tokens": (
1693
+ estimated_live_conversation_history_tokens
1694
+ ),
1695
+ "inline_tool_transcript_tokens": estimated_inline_tool_transcript_tokens,
1696
+ "memory_summary_tokens": estimated_memory_summary_tokens,
1697
+ "pins_tokens": estimated_pins_tokens,
1698
+ "total_tokens": estimated_total_request_tokens,
1699
+ "tool_schema_budget_reported_calls": tool_schema_budget_reported_calls,
1700
+ "tool_schema_budget_exceeded_calls": tool_schema_budget_exceeded_calls,
1701
+ "tool_schema_budget_overage_tokens": tool_schema_budget_overage_tokens,
1702
+ "tool_schema_largest_tool_tokens": tool_schema_largest_tool_tokens,
1703
+ },
1704
+ "calls": self.calls,
1705
+ }
1706
+
1707
+ def recent_calibration_snapshot(
1708
+ self,
1709
+ *,
1710
+ requested_model: str | None = None,
1711
+ provider_key: str | None = None,
1712
+ protocol: str | None = None,
1713
+ base_url_host: str | None = None,
1714
+ operation: str | None = None,
1715
+ request_mode: str | None = None,
1716
+ cache_strategy: str | None = None,
1717
+ limit: int = 20,
1718
+ ) -> dict[str, Any]:
1719
+ model_key = str(requested_model or "").strip()
1720
+ provider_key_filter = str(provider_key or "").strip()
1721
+ protocol_filter = str(protocol or "").strip()
1722
+ base_url_host_filter = str(base_url_host or "").strip()
1723
+ operation_filter = str(operation or "").strip()
1724
+ request_mode_filter = str(request_mode or "").strip()
1725
+ cache_strategy_filter = str(cache_strategy or "").strip()
1726
+ max_items = max(1, int(limit or 20))
1727
+ with self._lock:
1728
+ records = [
1729
+ record
1730
+ for record in self._records
1731
+ if _record_matches_calibration_filters(
1732
+ record,
1733
+ requested_model=model_key,
1734
+ provider_key=provider_key_filter,
1735
+ protocol=protocol_filter,
1736
+ base_url_host_filter=base_url_host_filter,
1737
+ operation=operation_filter,
1738
+ request_mode=request_mode_filter,
1739
+ cache_strategy=cache_strategy_filter,
1740
+ )
1741
+ ][-max_items:]
1742
+ snapshot = self._calibration_snapshot_from_records(records)
1743
+ snapshot["group_filter"] = {
1744
+ "requested_model": model_key,
1745
+ "provider_key": provider_key_filter,
1746
+ "protocol": protocol_filter,
1747
+ "base_url_host": base_url_host_filter,
1748
+ "operation": operation_filter,
1749
+ "request_mode": request_mode_filter,
1750
+ "cache_strategy": cache_strategy_filter,
1751
+ }
1752
+ return snapshot
1753
+
1754
+ def calibration_group_rows(self, *, limit: int = 200) -> list[dict[str, Any]]:
1755
+ max_items = max(1, int(limit or 200))
1756
+ with self._lock:
1757
+ records = self._records[-max_items:]
1758
+ groups: dict[tuple[str, str, str, str, str, str, str], list[UsageRecord]] = {}
1759
+ for record in records:
1760
+ groups.setdefault(_calibration_group_key(record), []).append(record)
1761
+ rows: list[dict[str, Any]] = []
1762
+ for key, group_records in sorted(groups.items()):
1763
+ snapshot = self._calibration_snapshot_from_records(group_records)
1764
+ rows.append(
1765
+ {
1766
+ "requested_model": key[0],
1767
+ "provider_key": key[1],
1768
+ "protocol": key[2],
1769
+ "base_url_host": key[3],
1770
+ "operation": key[4],
1771
+ "request_mode": key[5],
1772
+ "cache_strategy": key[6],
1773
+ **snapshot,
1774
+ }
1775
+ )
1776
+ return rows
1777
+
1778
+ def _calibration_snapshot_from_records(
1779
+ self,
1780
+ records: list[UsageRecord],
1781
+ ) -> dict[str, Any]:
1782
+ media_known_records = [record for record in records if record.usage_schema_version >= 5]
1783
+ calibration_records = [
1784
+ record for record in media_known_records if not record.request_has_media
1785
+ ]
1786
+ raw_ratios = [
1787
+ float(record.prompt_estimate_error_ratio)
1788
+ for record in calibration_records
1789
+ if record.prompt_estimate_error_ratio is not None
1790
+ and math.isfinite(float(record.prompt_estimate_error_ratio))
1791
+ and float(record.prompt_estimate_error_ratio) > 0
1792
+ ]
1793
+ ratios = _robust_calibration_ratios(raw_ratios)
1794
+ api_prompt_tokens = sum(max(0, record.prompt_tokens) for record in records)
1795
+ cache_read_tokens = sum(max(0, record.cache_read_input_tokens or 0) for record in records)
1796
+ cache_creation_tokens = sum(
1797
+ max(0, record.cache_creation_input_tokens or 0) for record in records
1798
+ )
1799
+ underestimate_calls = sum(
1800
+ 1 for record in records if (record.prompt_estimate_error_tokens or 0) > 0
1801
+ )
1802
+ overestimate_calls = sum(
1803
+ 1 for record in records if (record.prompt_estimate_error_tokens or 0) < 0
1804
+ )
1805
+ ratios_sorted = sorted(ratios)
1806
+ p90 = None
1807
+ if ratios_sorted:
1808
+ p90_index = min(
1809
+ len(ratios_sorted) - 1,
1810
+ max(0, math.ceil(len(ratios_sorted) * 0.9) - 1),
1811
+ )
1812
+ p90 = ratios_sorted[p90_index]
1813
+ return {
1814
+ "records": len(records),
1815
+ "calibration_calls": len(raw_ratios),
1816
+ "calibration_usable_calls": len(ratios),
1817
+ "calibration_outlier_calls": max(0, len(raw_ratios) - len(ratios)),
1818
+ "calibration_excluded_unknown_media_calls": (len(records) - len(media_known_records)),
1819
+ "calibration_excluded_media_calls": (
1820
+ len(media_known_records) - len(calibration_records)
1821
+ ),
1822
+ "calibration_stable": len(ratios) >= 3,
1823
+ "prompt_estimate_error_ratio_avg": (sum(ratios) / len(ratios) if ratios else None),
1824
+ "prompt_estimate_error_ratio_p90": p90,
1825
+ "prompt_estimate_error_ratio_max": max(ratios) if ratios else None,
1826
+ "prompt_estimate_error_ratio_raw_max": (max(raw_ratios) if raw_ratios else None),
1827
+ "prompt_estimate_underestimate_calls": underestimate_calls,
1828
+ "prompt_estimate_overestimate_calls": overestimate_calls,
1829
+ "cache_read_input_tokens": cache_read_tokens,
1830
+ "cache_creation_input_tokens": cache_creation_tokens,
1831
+ "cache_hit_ratio": (
1832
+ cache_read_tokens / api_prompt_tokens if api_prompt_tokens > 0 else None
1833
+ ),
1834
+ }
1835
+
1836
+
1837
+ def aggregate_usage_from_session_logs(paths: list[Path]) -> UsageSummary:
1838
+ summary = UsageSummary()
1839
+ for path in paths:
1840
+ if not path.exists() or not path.is_file():
1841
+ continue
1842
+ for event in read_session_events(path):
1843
+ if str(event.get("type") or "") != "llm_usage":
1844
+ continue
1845
+ payload = event.get("payload")
1846
+ if isinstance(payload, dict):
1847
+ summary.add_event_payload(payload)
1848
+ return summary
1849
+
1850
+
1851
+ def compute_context_left(
1852
+ *,
1853
+ messages: list[dict[str, Any]],
1854
+ model_name: str,
1855
+ registry: ModelRegistry,
1856
+ tool_list: list[dict[str, Any]] | None = None,
1857
+ pinned_prefix_len: int = 0,
1858
+ safety_margin_tokens: int = 512,
1859
+ startup_baseline_tokens: int = 0,
1860
+ prompt_estimate_multiplier: float | None = None,
1861
+ request_measurement: RequestContextMeasurement | None = None,
1862
+ ) -> ContextLeft:
1863
+ meta = registry.get(model_name)
1864
+ context_window = meta.context_window_tokens
1865
+ effective_input_budget = compute_input_budget(meta, safety_margin=safety_margin_tokens)
1866
+ baseline = max(0, int(startup_baseline_tokens or 0))
1867
+ request_estimate = estimate_request_token_breakdown(
1868
+ messages=messages,
1869
+ tool_list=tool_list,
1870
+ pinned_prefix_len=pinned_prefix_len,
1871
+ )
1872
+ local_used = request_estimate.total_tokens
1873
+ used = local_used
1874
+ token_count_source = UsageSource.LOCAL_ESTIMATE.value
1875
+ token_count_confidence = UsageConfidence.ESTIMATED.value
1876
+ multiplier = 1.0
1877
+ projection_kind = (
1878
+ request_measurement.projection_kind(messages=messages, tool_list=tool_list)
1879
+ if request_measurement is not None
1880
+ else None
1881
+ )
1882
+ anchor_token_count_source: str | None = None
1883
+ anchor_token_count_confidence: str | None = None
1884
+ provider_projection_applied = False
1885
+ if request_measurement is not None and projection_kind is not None:
1886
+ anchor_token_count_source = request_measurement.source
1887
+ anchor_token_count_confidence = request_measurement.confidence
1888
+ if projection_kind == "exact_request":
1889
+ used = request_measurement.input_tokens
1890
+ token_count_source = request_measurement.source
1891
+ token_count_confidence = request_measurement.confidence
1892
+ else:
1893
+ scale = request_measurement.input_tokens / max(
1894
+ 1,
1895
+ request_measurement.anchor_estimate_tokens,
1896
+ )
1897
+ persistent_delta = max(
1898
+ 0,
1899
+ local_used - request_measurement.persistent_anchor_estimate_tokens,
1900
+ )
1901
+ used = max(
1902
+ 0,
1903
+ request_measurement.input_tokens + math.ceil(persistent_delta * scale),
1904
+ )
1905
+ if request_measurement.source == UsageSource.LOCAL_ESTIMATE.value:
1906
+ token_count_source = UsageSource.LOCAL_ESTIMATE.value
1907
+ token_count_confidence = UsageConfidence.ESTIMATED.value
1908
+ else:
1909
+ token_count_source = UsageSource.MIXED.value
1910
+ token_count_confidence = UsageConfidence.ESTIMATED.value
1911
+ provider_projection_applied = True
1912
+ elif isinstance(prompt_estimate_multiplier, int | float):
1913
+ # Calibration is allowed to make the universal fallback more
1914
+ # conservative, never more optimistic. The caller supplies a
1915
+ # provider/protocol/operation-scoped robust percentile, so do not cap a
1916
+ # measured tokenizer gap at an arbitrary universal ratio.
1917
+ multiplier = max(1.0, float(prompt_estimate_multiplier))
1918
+ used = math.ceil(used * multiplier)
1919
+ baseline = math.ceil(baseline * multiplier)
1920
+ context_remaining = max(0, context_window - used) if context_window > 0 else None
1921
+ context_percent = (
1922
+ (context_remaining / context_window) * 100.0
1923
+ if (context_remaining is not None and context_window > 0)
1924
+ else None
1925
+ )
1926
+ effective_remaining = (
1927
+ max(0, effective_input_budget - used) if effective_input_budget > 0 else None
1928
+ )
1929
+ effective_percent = (
1930
+ (effective_remaining / effective_input_budget) * 100.0
1931
+ if (effective_remaining is not None and effective_input_budget > 0)
1932
+ else None
1933
+ )
1934
+ # Conversation-growth metrics intentionally remain on the local estimator.
1935
+ # Provider counts include tokenizer framing and ephemeral request wrappers;
1936
+ # attributing that overhead to conversation growth makes the dynamic gauge
1937
+ # jump even when no persistent context changed.
1938
+ dynamic_metric_used = local_used if projection_kind is not None else used
1939
+ dynamic_budget = max(0, effective_input_budget - baseline)
1940
+ dynamic_used = max(0, dynamic_metric_used - baseline)
1941
+ dynamic_remaining = max(0, dynamic_budget - dynamic_used)
1942
+ dynamic_percent = (dynamic_remaining / dynamic_budget) * 100.0 if dynamic_budget > 0 else 0.0
1943
+ return ContextLeft(
1944
+ model_name=model_name,
1945
+ max_input_tokens=context_window,
1946
+ used_input_tokens=used,
1947
+ remaining_tokens=context_remaining,
1948
+ percent_left=context_percent,
1949
+ source=meta.source,
1950
+ context_window_tokens=context_window,
1951
+ context_window_remaining_tokens=context_remaining,
1952
+ context_window_percent_left=context_percent,
1953
+ effective_input_budget=effective_input_budget,
1954
+ effective_remaining_tokens=effective_remaining,
1955
+ effective_percent_left=effective_percent,
1956
+ startup_baseline_tokens=baseline,
1957
+ dynamic_context_budget_tokens=dynamic_budget,
1958
+ dynamic_context_used_tokens=dynamic_used,
1959
+ dynamic_context_remaining_tokens=dynamic_remaining,
1960
+ dynamic_context_percent_left=dynamic_percent,
1961
+ token_count_source=token_count_source,
1962
+ token_count_confidence=token_count_confidence,
1963
+ local_request_estimate_tokens=local_used,
1964
+ anchor_token_count_source=anchor_token_count_source,
1965
+ anchor_token_count_confidence=anchor_token_count_confidence,
1966
+ provider_projection_applied=provider_projection_applied,
1967
+ capacity_provider_key=meta.provider_key,
1968
+ context_window_source=meta.field_sources.get("context_window_tokens", meta.source),
1969
+ max_output_tokens=meta.max_output_tokens,
1970
+ max_output_source=meta.field_sources.get("max_output_tokens", meta.source),
1971
+ safety_margin_tokens=max(0, int(safety_margin_tokens)),
1972
+ )
1973
+
1974
+
1975
+ def format_usd(cost: float | None, style: str = "table") -> str:
1976
+ if cost is None:
1977
+ return "n/a"
1978
+ decimals = 3 if style == "hud" else 4
1979
+ return f"${cost:.{decimals}f}"
1980
+
1981
+
1982
+ def format_cost(cost: float | None) -> str:
1983
+ # Backward-compatible alias for existing call sites.
1984
+ return format_usd(cost, style="table")
1985
+
1986
+
1987
+ def format_context_percent(ctx: ContextLeft) -> str:
1988
+ if ctx.percent_left is None:
1989
+ return "n/a"
1990
+ return f"{ctx.percent_left:.1f}%"