tokenjam 0.4.2__tar.gz → 0.5.1__tar.gz

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 (328) hide show
  1. {tokenjam-0.4.2 → tokenjam-0.5.1}/CLAUDE.md +2 -1
  2. {tokenjam-0.4.2 → tokenjam-0.5.1}/CONTRIBUTING.md +17 -7
  3. {tokenjam-0.4.2 → tokenjam-0.5.1}/PKG-INFO +26 -2
  4. {tokenjam-0.4.2 → tokenjam-0.5.1}/README.md +25 -1
  5. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/claude-code-integration.md +2 -2
  6. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/installation.md +3 -3
  7. tokenjam-0.5.1/docs/internal/release-smoke-checklist.md +126 -0
  8. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/trim.md +1 -1
  9. {tokenjam-0.4.2 → tokenjam-0.5.1}/pyproject.toml +1 -1
  10. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/package.json +1 -1
  11. tokenjam-0.5.1/tests/integration/test_analytics.py +191 -0
  12. tokenjam-0.5.1/tests/integration/test_cmd_route.py +163 -0
  13. tokenjam-0.5.1/tests/integration/test_cost_charts.py +198 -0
  14. tokenjam-0.5.1/tests/integration/test_first_run_roundtrip_239.py +303 -0
  15. tokenjam-0.5.1/tests/unit/test_backfill.py +437 -0
  16. tokenjam-0.5.1/tests/unit/test_export_route_generators.py +175 -0
  17. tokenjam-0.5.1/tests/unit/test_lens_ui_regression.py +904 -0
  18. tokenjam-0.5.1/tests/unit/test_onboard_first_run.py +155 -0
  19. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_workflow_restructure.py +25 -0
  20. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/app.py +2 -0
  21. tokenjam-0.5.1/tokenjam/api/routes/analytics.py +289 -0
  22. tokenjam-0.5.1/tokenjam/api/routes/cost.py +468 -0
  23. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/traces.py +4 -0
  24. tokenjam-0.5.1/tokenjam/cli/banner.py +36 -0
  25. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_backfill.py +14 -3
  26. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_onboard.py +89 -32
  27. tokenjam-0.5.1/tokenjam/cli/cmd_route.py +239 -0
  28. tokenjam-0.5.1/tokenjam/cli/home.py +46 -0
  29. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/main.py +14 -0
  30. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/backfill.py +66 -10
  31. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/db.py +4 -1
  32. tokenjam-0.5.1/tokenjam/core/export/ccr.py +106 -0
  33. tokenjam-0.5.1/tokenjam/core/export/common.py +101 -0
  34. tokenjam-0.5.1/tokenjam/core/export/litellm.py +102 -0
  35. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/models.py +6 -0
  36. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/workflow_restructure.py +13 -1
  37. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/index.html +1367 -236
  38. tokenjam-0.4.2/tests/unit/test_backfill.py +0 -231
  39. tokenjam-0.4.2/tests/unit/test_lens_ui_regression.py +0 -291
  40. tokenjam-0.4.2/tokenjam/api/routes/cost.py +0 -157
  41. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/CODEOWNERS +0 -0
  42. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
  45. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/pull_request_template.md +0 -0
  46. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/scripts/archive_traffic.py +0 -0
  47. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/ci.yml +0 -0
  48. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/publish-npm.yml +0 -0
  49. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/publish-pypi.yml +0 -0
  50. {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/traffic-archive.yml +0 -0
  51. {tokenjam-0.4.2 → tokenjam-0.5.1}/.gitignore +0 -0
  52. {tokenjam-0.4.2 → tokenjam-0.5.1}/AGENTS.md +0 -0
  53. {tokenjam-0.4.2 → tokenjam-0.5.1}/LICENSE +0 -0
  54. {tokenjam-0.4.2 → tokenjam-0.5.1}/Makefile +0 -0
  55. {tokenjam-0.4.2 → tokenjam-0.5.1}/SECURITY.md +0 -0
  56. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/alerts.md +0 -0
  57. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/architecture.md +0 -0
  58. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/helicone.md +0 -0
  59. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/langfuse.md +0 -0
  60. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/otlp.md +0 -0
  61. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/overview.md +0 -0
  62. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/cli-reference.md +0 -0
  63. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/configuration.md +0 -0
  64. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/export.md +0 -0
  65. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/framework-support.md +0 -0
  66. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/internal/lens-vendor-versions.md +0 -0
  67. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/internal/specs/.gitkeep +0 -0
  68. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/nemoclaw-integration.md +0 -0
  69. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/openclaw.md +0 -0
  70. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/cache.md +0 -0
  71. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/downsize.md +0 -0
  72. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/reuse.md +0 -0
  73. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/script.md +0 -0
  74. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/policy/overview.md +0 -0
  75. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/python-sdk.md +0 -0
  76. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-alerts.png +0 -0
  77. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-budget.png +0 -0
  78. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-cost.png +0 -0
  79. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-status.png +0 -0
  80. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-traces.png +0 -0
  81. {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/typescript-sdk.md +0 -0
  82. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/README.md +0 -0
  83. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/_shared.py +0 -0
  84. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
  85. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/drift_demo.py +0 -0
  86. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
  87. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/rag_pipeline.py +0 -0
  88. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/research_team.py +0 -0
  89. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/router_agent.py +0 -0
  90. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/agent_patterns.txt +0 -0
  91. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/cost_management.txt +0 -0
  92. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/observability.txt +0 -0
  93. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/safety.txt +0 -0
  94. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/openclaw/README.md +0 -0
  95. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/autogen_agent.py +0 -0
  96. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/crewai_agent.py +0 -0
  97. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/langchain_agent.py +0 -0
  98. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/langgraph_agent.py +0 -0
  99. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/llamaindex_agent.py +0 -0
  100. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/anthropic_agent.py +0 -0
  101. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/bedrock_agent.py +0 -0
  102. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/gemini_agent.py +0 -0
  103. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/litellm_agent.py +0 -0
  104. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/openai_agent.py +0 -0
  105. {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
  106. {tokenjam-0.4.2 → tokenjam-0.5.1}/growth/README.md +0 -0
  107. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/hallucination-drift/BLOG.md +0 -0
  108. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/hallucination-drift/README.md +0 -0
  109. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/hallucination-drift/scenario.py +0 -0
  110. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/retry-loop/BLOG.md +0 -0
  111. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/retry-loop/README.md +0 -0
  112. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/retry-loop/scenario.py +0 -0
  113. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/surprise-cost/BLOG.md +0 -0
  114. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/surprise-cost/README.md +0 -0
  115. {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/surprise-cost/scenario.py +0 -0
  116. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/README.md +0 -0
  117. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/package-lock.json +0 -0
  118. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/client.test.ts +0 -0
  119. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/client.ts +0 -0
  120. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/index.ts +0 -0
  121. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/semconv.test.ts +0 -0
  122. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/semconv.ts +0 -0
  123. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/span-builder.test.ts +0 -0
  124. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/span-builder.ts +0 -0
  125. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/types.ts +0 -0
  126. {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/tsconfig.json +0 -0
  127. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/__init__.py +0 -0
  128. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-post-release-runner.md +0 -0
  129. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-post-release-v0.4.0.md +0 -0
  130. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-pre-release-runner.md +0 -0
  131. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-pre-release-v0.4.0.md +0 -0
  132. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-pre-release-v0.4.1.md +0 -0
  133. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/__init__.py +0 -0
  134. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_budget_breach.py +0 -0
  135. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_drift.py +0 -0
  136. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_loop.py +0 -0
  137. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_normal.py +0 -0
  138. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/mock_llm.py +0 -0
  139. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/test_mock_scenarios.py +0 -0
  140. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/conftest.py +0 -0
  141. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/e2e/__init__.py +0 -0
  142. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/e2e/conftest.py +0 -0
  143. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/e2e/test_real_llm.py +0 -0
  144. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/factories.py +0 -0
  145. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/fixtures/helicone_real_response.json +0 -0
  146. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/fixtures/langfuse_real_response.json +0 -0
  147. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/fixtures/otlp_sample.json +0 -0
  148. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/__init__.py +0 -0
  149. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_api.py +0 -0
  150. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_cli.py +0 -0
  151. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_cli_api_framing_parity.py +0 -0
  152. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_cost_framing_window.py +0 -0
  153. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_db.py +0 -0
  154. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_demos.py +0 -0
  155. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_full_pipeline.py +0 -0
  156. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_litellm_enrichment.py +0 -0
  157. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_logs_api.py +0 -0
  158. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_sdk_config_discovery.py +0 -0
  159. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/manual-new-release-tests.md +0 -0
  160. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/manual-pre-release-testing.md +0 -0
  161. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-post-release-v0.4.0-20260619T221412Z.md +0 -0
  162. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-pre-release-v0.4.0-20260619T202233Z.md +0 -0
  163. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-pre-release-v0.4.0-20260619T205108Z.md +0 -0
  164. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md +0 -0
  165. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/__init__.py +0 -0
  166. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_alert_rules.py +0 -0
  167. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_cost_tracking.py +0 -0
  168. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_drift_detection.py +0 -0
  169. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_ingest.py +0 -0
  170. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_schema_validation.py +0 -0
  171. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/toy_agent/toy_agent.py +0 -0
  172. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/__init__.py +0 -0
  173. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_alerts.py +0 -0
  174. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cache_efficacy.py +0 -0
  175. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cache_recommend.py +0 -0
  176. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_cost.py +0 -0
  177. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_policy.py +0 -0
  178. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_stop.py +0 -0
  179. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_tokenmaxx.py +0 -0
  180. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_compare.py +0 -0
  181. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_config.py +0 -0
  182. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_config_secret_divergence.py +0 -0
  183. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cost.py +0 -0
  184. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cost_compare_framing.py +0 -0
  185. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cycle.py +0 -0
  186. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_db_helpers.py +0 -0
  187. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_demo_env.py +0 -0
  188. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_demo_scenarios.py +0 -0
  189. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_drift.py +0 -0
  190. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_export_claude_code.py +0 -0
  191. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_formatting.py +0 -0
  192. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_framing.py +0 -0
  193. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ingest_helicone.py +0 -0
  194. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ingest_langfuse.py +0 -0
  195. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ingest_otlp.py +0 -0
  196. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_litellm_client.py +0 -0
  197. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_litellm_integration.py +0 -0
  198. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_logs_converter.py +0 -0
  199. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_mcp_server.py +0 -0
  200. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_models.py +0 -0
  201. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_no_tracked_dev_secrets.py +0 -0
  202. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_codex.py +0 -0
  203. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_daemon.py +0 -0
  204. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_hygiene.py +0 -0
  205. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_plan.py +0 -0
  206. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_openclaw_ingest.py +0 -0
  207. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_optimize.py +0 -0
  208. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_optimize_recoverable.py +0 -0
  209. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_optimize_reuse.py +0 -0
  210. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_plan_tier_apply.py +0 -0
  211. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_pricing_override.py +0 -0
  212. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_prompt_bloat.py +0 -0
  213. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_provider_attribution_194.py +0 -0
  214. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_reuse_skeleton.py +0 -0
  215. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_spans_stats_repair.py +0 -0
  216. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_time_parse.py +0 -0
  217. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_transport_401.py +0 -0
  218. {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ui_offline.py +0 -0
  219. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/__init__.py +0 -0
  220. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/__init__.py +0 -0
  221. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/deps.py +0 -0
  222. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/middleware.py +0 -0
  223. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/__init__.py +0 -0
  224. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/agents.py +0 -0
  225. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/alerts.py +0 -0
  226. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/budget.py +0 -0
  227. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/cost_compare.py +0 -0
  228. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/drift.py +0 -0
  229. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/logs.py +0 -0
  230. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/metrics.py +0 -0
  231. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/optimize.py +0 -0
  232. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/otlp.py +0 -0
  233. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/reuse.py +0 -0
  234. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/spans.py +0 -0
  235. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/status.py +0 -0
  236. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/tools.py +0 -0
  237. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/version.py +0 -0
  238. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/__init__.py +0 -0
  239. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_alerts.py +0 -0
  240. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_budget.py +0 -0
  241. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_cost.py +0 -0
  242. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_demo.py +0 -0
  243. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_doctor.py +0 -0
  244. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_drift.py +0 -0
  245. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_export.py +0 -0
  246. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_mcp.py +0 -0
  247. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_optimize.py +0 -0
  248. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_policy.py +0 -0
  249. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_report.py +0 -0
  250. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_serve.py +0 -0
  251. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_status.py +0 -0
  252. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_stop.py +0 -0
  253. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_tokenmaxx.py +0 -0
  254. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_tools.py +0 -0
  255. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_traces.py +0 -0
  256. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_uninstall.py +0 -0
  257. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/__init__.py +0 -0
  258. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/alerts.py +0 -0
  259. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/api_backend.py +0 -0
  260. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/config.py +0 -0
  261. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/cost.py +0 -0
  262. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/cycle.py +0 -0
  263. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/drift.py +0 -0
  264. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/__init__.py +0 -0
  265. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/claude_code.py +0 -0
  266. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/reuse_report.py +0 -0
  267. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/reuse_skeleton.py +0 -0
  268. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/framing.py +0 -0
  269. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest.py +0 -0
  270. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/__init__.py +0 -0
  271. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/helicone.py +0 -0
  272. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
  273. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/otlp.py +0 -0
  274. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/README.md +0 -0
  275. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/__init__.py +0 -0
  276. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
  277. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
  278. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
  279. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
  280. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/model_downgrade.py +0 -0
  281. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/plan_reuse.py +0 -0
  282. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
  283. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/registry.py +0 -0
  284. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/runner.py +0 -0
  285. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/types.py +0 -0
  286. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/pricing.py +0 -0
  287. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/retention.py +0 -0
  288. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/schema_validator.py +0 -0
  289. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/demo/__init__.py +0 -0
  290. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/demo/env.py +0 -0
  291. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/mcp/__init__.py +0 -0
  292. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/mcp/server.py +0 -0
  293. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/__init__.py +0 -0
  294. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/exporters.py +0 -0
  295. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/otlp_parsing.py +0 -0
  296. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/provider.py +0 -0
  297. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/semconv.py +0 -0
  298. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/pricing/models.toml +0 -0
  299. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/py.typed +0 -0
  300. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/__init__.py +0 -0
  301. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/agent.py +0 -0
  302. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/bootstrap.py +0 -0
  303. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/client.py +0 -0
  304. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/http_exporter.py +0 -0
  305. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/__init__.py +0 -0
  306. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/anthropic.py +0 -0
  307. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/autogen.py +0 -0
  308. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/base.py +0 -0
  309. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/bedrock.py +0 -0
  310. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/crewai.py +0 -0
  311. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/gemini.py +0 -0
  312. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/langchain.py +0 -0
  313. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/langgraph.py +0 -0
  314. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/litellm.py +0 -0
  315. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/llamaindex.py +0 -0
  316. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
  317. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/openai.py +0 -0
  318. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
  319. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/transport.py +0 -0
  320. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/htm.js +0 -0
  321. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/preact-hooks.js +0 -0
  322. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/preact.js +0 -0
  323. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/uplot.css +0 -0
  324. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/uplot.js +0 -0
  325. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/__init__.py +0 -0
  326. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/formatting.py +0 -0
  327. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/ids.py +0 -0
  328. {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/time_parse.py +0 -0
@@ -376,7 +376,7 @@ Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column ha
376
376
  - `tokenjam[bloat]` — `llmlingua>=0.2`, used by the Trim analyzer. Pulls PyTorch + transformers (~2GB). Kept out of base install. The analyzer self-registers without the extra installed; the deferred `import llmlingua` inside the analysis function body raises a typed message pointing the user at the install command.
377
377
  - Framework extras `[langchain]`, `[crewai]`, `[autogen]`, `[litellm]` for SDK patches.
378
378
  - `[dev]` for local development (`pytest`, `ruff`, `mypy`, `httpx`).
379
- - `[mcp]` for the FastMCP stdio server.
379
+ - `[mcp]` empty no-op alias. `fastmcp` moved into the base install in v0.3.5 (#101), so the FastMCP stdio server (`tj mcp`) works on a plain `pipx install tokenjam`. The extra is kept so old `tokenjam[mcp]` install commands still succeed; it pulls nothing extra.
380
380
 
381
381
  ## Further Reading
382
382
 
@@ -392,3 +392,4 @@ Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column ha
392
392
  - **Backfill adapters** — `docs/backfill/overview.md` lists the four sources (`claude-code` / `langfuse` / `helicone` / `otlp`) with the partnership-posture framing; per-adapter pages document modes (URL / file), field mapping, idempotency, and v1 limitations.
393
393
  - **[docs/policy/overview.md](docs/policy/overview.md)** — read-only preview of the unified policy surface (`tj policy list`). Notes that the `add` / `edit` / `apply` subcommands and the underlying `[policy]` config migration land next sprint.
394
394
  - **Internal specs** — `docs/internal/specs/` is reserved for canonical specs that production code references at long-term. Currently empty (sprint specs have been cleaned up after merge); add new ones here when a feature needs a stable, code-referenced source of truth.
395
+ - **[docs/internal/release-smoke-checklist.md](docs/internal/release-smoke-checklist.md)** — fresh-install pre-release gate (clean env → `pipx install` → `tj onboard --claude-code` → verify Lens plan badge + `tj optimize` agree → sane session count). Catches plan-tier / framing regressions a real first-run hits that synthetic-factory tests don't. Its automated counterpart is `tests/integration/test_first_run_roundtrip_239.py`.
@@ -1,14 +1,24 @@
1
1
  # Contributing to TokenJam
2
2
 
3
- TokenJam is MIT licensed and welcomes contributions. The codebase was built using parallel Claude Code agents the `.claude/` task files are intentionally committed so contributors can use the same workflow.
3
+ TokenJam is MIT licensed and welcomes contributions. It's built by AI coding agents, and contributing with one is first-class see [Using coding agents](#using-coding-agents) below.
4
+
5
+ ## Good first contributions
6
+
7
+ New here? These are the lowest-friction ways to land your first PR:
8
+
9
+ - 🟢 **[Good first issues](https://github.com/Metabuilder-Labs/tokenjam/labels/good%20first%20issue)** — scoped tasks tagged ready for newcomers.
10
+ - 💸 **Model pricing** — add a missing model or correct a rate in [`tokenjam/pricing/models.toml`](tokenjam/pricing/models.toml). One file, no issue needed (details below).
11
+ - 🔌 **A framework/provider integration** — they follow one clear pattern; `tokenjam/sdk/integrations/anthropic.py` is the reference. Open an issue first to align on approach.
12
+
13
+ Questions on any of these? Open an issue — we're happy to point you at a good starting spot.
4
14
 
5
15
  ## Getting started
6
16
 
7
17
  ```bash
8
18
  git clone https://github.com/Metabuilder-Labs/tokenjam
9
19
  cd tokenjam
10
- pip install -e ".[dev,mcp]" # editable install with dev tools + MCP support
11
- pip install anthropic # for running the toy agent
20
+ pip install -e ".[dev]" # editable install with dev tools (the MCP server ships in the base install)
21
+ pip install anthropic # for running the toy agent
12
22
  ```
13
23
 
14
24
  ## Running tests
@@ -48,12 +58,12 @@ tests/factories.py Span factory — use this in all synthetic tests, never
48
58
  construct NormalizedSpan directly
49
59
  ```
50
60
 
51
- ## Using Claude Code
61
+ ## Using coding agents
52
62
 
53
- This project was built using parallel Claude Code agents. The `.claude/` directory contains the original task files. If you're using Claude Code to contribute:
63
+ TokenJam is built by AI coding agents, and contributing with one is first-class:
54
64
 
55
- - Read `AGENTS.md` at the repo root before starting — it contains the critical rules (DuckDB not SQLite, TOML not YAML, no CLI imports in `tokenjam/core/`, etc.)
56
- - The task files in `.claude/` show how the codebase was structured and are useful context for larger contributions
65
+ - **Claude Code** — it reads [`CLAUDE.md`](CLAUDE.md) automatically; run `/init` to bring your agent up to speed on the architecture and conventions.
66
+ - **Codex / Gemini / other agents** read [`AGENTS.md`](AGENTS.md): the critical rules (DuckDB not SQLite, TOML not YAML, no CLI imports in `tokenjam/core/`, etc.) plus a pointer to CLAUDE.md for the full guide.
57
67
 
58
68
  ## Pricing table contributions
59
69
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenjam
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: TokenJam — local-first OTel-native observability for Autonomous AI agents
5
5
  Project-URL: Homepage, https://opencla.watch
6
6
  Project-URL: Repository, https://github.com/Metabuilder-Labs/openclawwatch
@@ -68,6 +68,7 @@ TokenJam reads your agent's telemetry and tells you when to downsize, when to tr
68
68
 
69
69
  [![CI](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml/badge.svg)](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml)
70
70
  [![PyPI](https://img.shields.io/pypi/v/tokenjam?color=3d8eff&labelColor=0d1117)](https://pypi.org/project/tokenjam/)
71
+ [![Downloads](https://img.shields.io/pypi/dm/tokenjam?color=3d8eff&labelColor=0d1117&label=downloads)](https://pypi.org/project/tokenjam/)
71
72
  [![Python](https://img.shields.io/badge/python-3.10%2B-3d8eff?labelColor=0d1117)](https://pypi.org/project/tokenjam/)
72
73
  [![npm](https://img.shields.io/npm/v/@tokenjam/sdk?color=3d8eff&labelColor=0d1117)](https://www.npmjs.com/package/@tokenjam/sdk)
73
74
  [![License: MIT](https://img.shields.io/badge/license-MIT-3d8eff?labelColor=0d1117)](LICENSE)
@@ -81,6 +82,8 @@ pipx install tokenjam
81
82
 
82
83
  **No cloud · No signup · No vendor lock-in**
83
84
 
85
+ <sub>⭐ If TokenJam saves you tokens, **star it** · 👁 **Watch for releases** — we ship often</sub>
86
+
84
87
  </div>
85
88
 
86
89
  ---
@@ -280,7 +283,13 @@ tj serve # start Lens + REST API
280
283
  - [x] **Daemon DB concurrency** — per-thread DuckDB cursors so the Overview's fan-out doesn't block on a single shared connection (v0.4.1)
281
284
  - [x] **Cache cost transparency** — `cache_read` + `cache_write` token columns surfaced in CLI + UI + API (the previously-hidden ~91% cost driver on cache-heavy workloads)
282
285
 
283
- **Up next:**
286
+ **Shipped in 0.5.x:**
287
+ - [x] **Lens Visualizations** — an Analytics pivot explorer (metric × dimension × chart, presets, CSV), stacked cost-by-model, a cache-savings chart, KPI sparklines, a cost-annotated trace waterfall, and consistent series coloring
288
+ - [x] **Merged Dashboard** — the explorer and the triage front-door unified into one default screen, with in-place drill-through from recoverable-waste tiles
289
+ - [x] **First-run polish** — backfill fidelity (session-level traces, cache read/write split, honest session counts), plan-tier-aware framing throughout (subscription users see token-share, never raw spend), an onboarding welcome banner + next-steps guidance, and a contribution funnel
290
+
291
+ **Up next** (roughly):
292
+ - [ ] Continued Lens polish + per-product visual branding
284
293
  - [ ] `tj policy add | edit | apply` — unified rule surface
285
294
  - [ ] `tj replay` — replay captured sessions against new model versions
286
295
  - [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
@@ -290,6 +299,21 @@ tj serve # start Lens + REST API
290
299
 
291
300
  ---
292
301
 
302
+ ## Contributing
303
+
304
+ TokenJam is MIT, and contributions are welcome — from a one-line pricing fix to a whole new framework integration. A few easy on-ramps:
305
+
306
+ - 🟢 **[Good first issues →](https://github.com/Metabuilder-Labs/tokenjam/labels/good%20first%20issue)** — scoped, newcomer-friendly tasks, ready to pick up.
307
+ - 💸 **Model pricing** — `tokenjam/pricing/models.toml` is community-maintained. Fix a rate or add a model in a single PR — no issue needed.
308
+ - 🔌 **Framework integrations** — provider/framework patches follow one clear pattern (`tokenjam/sdk/integrations/anthropic.py` is the reference). Open an issue first to align on approach.
309
+ - 🤖 **Built with coding agents** — TokenJam is built by AI coding agents, and contributing with one is first-class. **Claude Code:** read [CLAUDE.md](CLAUDE.md) and run `/init` to bring your agent up to speed. **Codex / other agents:** [AGENTS.md](AGENTS.md) has the critical rules.
310
+
311
+ Setup and the full dev workflow are in **[CONTRIBUTING.md](CONTRIBUTING.md)**.
312
+
313
+ If TokenJam saves you tokens, **⭐ star it** and **👁 watch for releases** — we ship often.
314
+
315
+ ---
316
+
293
317
  <div align="center">
294
318
 
295
319
  **[tokenjam.dev](https://tokenjam.dev)** · [PyPI](https://pypi.org/project/tokenjam/) · [npm](https://www.npmjs.com/package/@tokenjam/sdk) · [Issues](https://github.com/Metabuilder-Labs/tokenjam/issues)
@@ -10,6 +10,7 @@ TokenJam reads your agent's telemetry and tells you when to downsize, when to tr
10
10
 
11
11
  [![CI](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml/badge.svg)](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml)
12
12
  [![PyPI](https://img.shields.io/pypi/v/tokenjam?color=3d8eff&labelColor=0d1117)](https://pypi.org/project/tokenjam/)
13
+ [![Downloads](https://img.shields.io/pypi/dm/tokenjam?color=3d8eff&labelColor=0d1117&label=downloads)](https://pypi.org/project/tokenjam/)
13
14
  [![Python](https://img.shields.io/badge/python-3.10%2B-3d8eff?labelColor=0d1117)](https://pypi.org/project/tokenjam/)
14
15
  [![npm](https://img.shields.io/npm/v/@tokenjam/sdk?color=3d8eff&labelColor=0d1117)](https://www.npmjs.com/package/@tokenjam/sdk)
15
16
  [![License: MIT](https://img.shields.io/badge/license-MIT-3d8eff?labelColor=0d1117)](LICENSE)
@@ -23,6 +24,8 @@ pipx install tokenjam
23
24
 
24
25
  **No cloud · No signup · No vendor lock-in**
25
26
 
27
+ <sub>⭐ If TokenJam saves you tokens, **star it** · 👁 **Watch for releases** — we ship often</sub>
28
+
26
29
  </div>
27
30
 
28
31
  ---
@@ -222,7 +225,13 @@ tj serve # start Lens + REST API
222
225
  - [x] **Daemon DB concurrency** — per-thread DuckDB cursors so the Overview's fan-out doesn't block on a single shared connection (v0.4.1)
223
226
  - [x] **Cache cost transparency** — `cache_read` + `cache_write` token columns surfaced in CLI + UI + API (the previously-hidden ~91% cost driver on cache-heavy workloads)
224
227
 
225
- **Up next:**
228
+ **Shipped in 0.5.x:**
229
+ - [x] **Lens Visualizations** — an Analytics pivot explorer (metric × dimension × chart, presets, CSV), stacked cost-by-model, a cache-savings chart, KPI sparklines, a cost-annotated trace waterfall, and consistent series coloring
230
+ - [x] **Merged Dashboard** — the explorer and the triage front-door unified into one default screen, with in-place drill-through from recoverable-waste tiles
231
+ - [x] **First-run polish** — backfill fidelity (session-level traces, cache read/write split, honest session counts), plan-tier-aware framing throughout (subscription users see token-share, never raw spend), an onboarding welcome banner + next-steps guidance, and a contribution funnel
232
+
233
+ **Up next** (roughly):
234
+ - [ ] Continued Lens polish + per-product visual branding
226
235
  - [ ] `tj policy add | edit | apply` — unified rule surface
227
236
  - [ ] `tj replay` — replay captured sessions against new model versions
228
237
  - [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
@@ -232,6 +241,21 @@ tj serve # start Lens + REST API
232
241
 
233
242
  ---
234
243
 
244
+ ## Contributing
245
+
246
+ TokenJam is MIT, and contributions are welcome — from a one-line pricing fix to a whole new framework integration. A few easy on-ramps:
247
+
248
+ - 🟢 **[Good first issues →](https://github.com/Metabuilder-Labs/tokenjam/labels/good%20first%20issue)** — scoped, newcomer-friendly tasks, ready to pick up.
249
+ - 💸 **Model pricing** — `tokenjam/pricing/models.toml` is community-maintained. Fix a rate or add a model in a single PR — no issue needed.
250
+ - 🔌 **Framework integrations** — provider/framework patches follow one clear pattern (`tokenjam/sdk/integrations/anthropic.py` is the reference). Open an issue first to align on approach.
251
+ - 🤖 **Built with coding agents** — TokenJam is built by AI coding agents, and contributing with one is first-class. **Claude Code:** read [CLAUDE.md](CLAUDE.md) and run `/init` to bring your agent up to speed. **Codex / other agents:** [AGENTS.md](AGENTS.md) has the critical rules.
252
+
253
+ Setup and the full dev workflow are in **[CONTRIBUTING.md](CONTRIBUTING.md)**.
254
+
255
+ If TokenJam saves you tokens, **⭐ star it** and **👁 watch for releases** — we ship often.
256
+
257
+ ---
258
+
235
259
  <div align="center">
236
260
 
237
261
  **[tokenjam.dev](https://tokenjam.dev)** · [PyPI](https://pypi.org/project/tokenjam/) · [npm](https://www.npmjs.com/package/@tokenjam/sdk) · [Issues](https://github.com/Metabuilder-Labs/tokenjam/issues)
@@ -3,7 +3,7 @@
3
3
  Monitor every Claude Code session — costs, tool calls, API requests, errors — with two commands:
4
4
 
5
5
  ```bash
6
- pipx install 'tokenjam[mcp]'
6
+ pipx install tokenjam
7
7
  tj onboard --claude-code
8
8
  # Restart Claude Code, then:
9
9
  tj status --agent claude-code-<project>
@@ -35,7 +35,7 @@ Claude Code emits OTLP log events which `tj serve` converts into spans — every
35
35
 
36
36
  ## MCP server
37
37
 
38
- The MCP server is included in the `[mcp]` extra and registered automatically by `tj onboard --claude-code`. It gives Claude Code direct access to your observability data inside the session itself. After restarting Claude Code you have 13 tools available in every session:
38
+ The MCP server ships in the base install (`pipx install tokenjam` — no extra needed) and is registered automatically by `tj onboard --claude-code`. It gives Claude Code direct access to your observability data inside the session itself. After restarting Claude Code you have 13 tools available in every session:
39
39
 
40
40
  | Tool | What it does |
41
41
  |---|---|
@@ -52,8 +52,8 @@ TokenJam keeps heavyweight ML dependencies, framework adapters, and the MCP serv
52
52
 
53
53
  | Extra | What it pulls in | Why it's optional |
54
54
  |---|---|---|
55
- | `tokenjam[mcp]` | `fastmcp` | Only needed for the Claude Code / Codex MCP server (`tj mcp`). Pulled by `tj onboard --claude-code` automatically when invoked through the documented one-liner. |
56
- | `tokenjam[bloat]` | `llmlingua>=0.2`, transitively PyTorch + transformers (~2GB) | The Trim analyzer (`tj optimize trim`) scores token significance with LLMLingua-2. Most users don't run it; keeping torch out of the base install means `pip install tokenjam` stays small and fast on machines that don't have a GPU/CPU build of torch already. |
55
+ | `tokenjam[mcp]` | nothing (no-op alias) | **No longer needed.** `fastmcp` moved into the base install in v0.3.5, so the Claude Code / Codex MCP server (`tj mcp`) works on a plain `pipx install tokenjam`. The extra is kept as an empty no-op so old `pipx install 'tokenjam[mcp]'` commands still succeed; it pulls nothing extra. |
56
+ | `tokenjam[bloat]` | `llmlingua>=0.2`, transitively PyTorch + transformers (~2GB) | The Trim analyzer (`tj optimize trim`) scores token significance with LLMLingua-2. Most users don't run it; keeping torch out of the base install keeps it small and fast on machines that don't have a GPU/CPU build of torch already. |
57
57
  | `tokenjam[langchain]` | `langchain>=0.2` | Convenience pin for `patch_langchain()`; you can also install langchain yourself. |
58
58
  | `tokenjam[crewai]` | `crewai>=0.28` | Convenience pin for `patch_crewai()`. |
59
59
  | `tokenjam[autogen]` | `pyautogen>=0.2` | Convenience pin for `patch_autogen()`. |
@@ -63,7 +63,7 @@ TokenJam keeps heavyweight ML dependencies, framework adapters, and the MCP serv
63
63
  Combine multiple extras:
64
64
 
65
65
  ```bash
66
- pipx install 'tokenjam[mcp,bloat]'
66
+ pipx install 'tokenjam[bloat,litellm]'
67
67
  ```
68
68
 
69
69
  ### Bloat extra details
@@ -0,0 +1,126 @@
1
+ # Fresh-install release smoke checklist
2
+
3
+ A lightweight pre-release gate so **plan-tier / framing regressions can't ship
4
+ silently**. The 0.5.0 first-run review burned real time chasing a "Max 20x"
5
+ framing that turned out to be stale data, not a code bug — there was no
6
+ automated *or* manual signal either way. Run this once against a clean machine
7
+ (or a throwaway VM / container) after publishing a release candidate and before
8
+ announcing the release.
9
+
10
+ This is distinct from [`tests/manual-pre-release-testing.md`](../../tests/manual-pre-release-testing.md),
11
+ which tests a **branch** via an editable install. This checklist tests the
12
+ **published wheel** the way a brand-new user installs it: `pipx install tokenjam`.
13
+
14
+ The automated counterpart to this checklist is
15
+ `tests/integration/test_first_run_roundtrip_239.py`, which pins the
16
+ config → backfill → framing contract in CI. This checklist covers the parts CI
17
+ can't: a real `pipx` install, a real `tj onboard --claude-code`, and the
18
+ rendered Lens badge.
19
+
20
+ ## 0. Clean environment
21
+
22
+ Start from a machine (or container / VM) with **no prior TokenJam state**. On a
23
+ machine you've used before, wipe it first:
24
+
25
+ ```bash
26
+ tj stop 2>/dev/null # halt any daemon holding the DB / port 7391
27
+ tj uninstall --yes 2>/dev/null # remove launchd / systemd unit files
28
+ pipx uninstall tokenjam 2>/dev/null
29
+ pip3 uninstall -y tokenjam 2>/dev/null
30
+ rm -rf ~/.tj ~/.config/tj .tj # default DB (~/.tj/telemetry.duckdb), global + project config
31
+ ```
32
+
33
+ Verify nothing lingers:
34
+
35
+ ```bash
36
+ launchctl list | grep tokenjam # macOS — expect no output
37
+ systemctl --user is-active tokenjam # Linux — expect "inactive"/"unknown"
38
+ which tj # expect "not found"
39
+ ```
40
+
41
+ ## 1. Fresh install
42
+
43
+ ```bash
44
+ pipx install tokenjam
45
+ tj --version # matches the release tag (vX.Y.Z)
46
+ ```
47
+
48
+ `pipx`, not `pip` — that's the recommended path (sidesteps PEP 668 on Homebrew
49
+ Python and Debian 12+/Ubuntu 24+).
50
+
51
+ ## 2. Onboard with a known plan
52
+
53
+ ```bash
54
+ tj onboard --claude-code --plan max_5x
55
+ ```
56
+
57
+ Use `--plan max_5x` (non-interactive) so the expected framing is unambiguous.
58
+ This writes `[budget.anthropic] plan = "max_5x"` to the **global** config
59
+ (`~/.config/tj/config.toml`) and auto-runs the Claude Code backfill at the end.
60
+
61
+ Confirm the config landed:
62
+
63
+ ```bash
64
+ grep -A2 'budget.anthropic' ~/.config/tj/config.toml # plan = "max_5x"
65
+ ```
66
+
67
+ > If this machine has no `~/.claude/projects/*.jsonl` history, seed a little by
68
+ > running Claude Code a few times first — otherwise the backfill finds nothing
69
+ > and the framing falls back to the config-declared plan (still `max_5x`, but
70
+ > you won't exercise the data path).
71
+
72
+ ## 3. Verify the backfill session count is sane
73
+
74
+ The onboard backfill prints a summary. Sanity-check it against the table — the
75
+ two must reconcile (the #238 contract):
76
+
77
+ ```bash
78
+ tj status # session count, span count, window
79
+ # Compare against the printed "N new / M existing · T total" summary.
80
+ ```
81
+
82
+ Red flags: a "1 session" summary on a machine with months of history (new-only
83
+ reporting — #238), or a session count wildly off from the spans/cost shown.
84
+
85
+ ## 4. Verify the Lens plan badge matches the config
86
+
87
+ ```bash
88
+ tj serve & # or rely on the daemon installed by onboard
89
+ open http://localhost:7391/ # macOS; otherwise browse to it
90
+ ```
91
+
92
+ On the **Overview** screen:
93
+
94
+ - The plan badge / framing reads **Max 5x plan** (subscription), **not** Max 20x
95
+ or an API-dollar framing.
96
+ - Cost figures render as token-share / "% of cycle", **not** raw API dollars
97
+ (subscription users never see "spend").
98
+
99
+ Optionally confirm the raw framing block:
100
+
101
+ ```bash
102
+ curl -s 'http://localhost:7391/api/v1/cost?since=90d' | python3 -m json.tool | grep -A12 '"framing"'
103
+ # expect: "pricing_mode": "subscription", "plan_tier": "max_5x", "plan_monthly_usd": 100.0
104
+ ```
105
+
106
+ ## 5. Verify `tj optimize` agrees with Lens
107
+
108
+ ```bash
109
+ tj optimize --since 90d
110
+ ```
111
+
112
+ - The framing must match the Lens badge: subscription / Max 5x, **implied API
113
+ value** language, token-share savings — never dollar "spend" or a different
114
+ plan than step 4 showed.
115
+ - Savings lines say "estimated recoverable", never "saves you" (Critical Rule 14).
116
+
117
+ If Lens and `tj optimize` disagree on the plan, **stop** — that's the exact
118
+ class of regression this gate exists to catch. File a blocker before releasing.
119
+
120
+ ## Pass criteria
121
+
122
+ - [ ] Clean machine → `pipx install tokenjam` succeeds; `tj --version` == release tag
123
+ - [ ] `tj onboard --claude-code --plan max_5x` writes `plan = "max_5x"` to global config
124
+ - [ ] Backfill summary reconciles with `tj status` (new/existing/total is sane, not new-only)
125
+ - [ ] Lens Overview badge reads **Max 5x** (subscription framing, no API dollars)
126
+ - [ ] `tj optimize` framing agrees with Lens (same plan, implied-value language)
@@ -25,7 +25,7 @@ base install:
25
25
  pipx install 'tokenjam[bloat]'
26
26
  ```
27
27
 
28
- The base `pip install tokenjam` does NOT pull torch. Trim shows up in
28
+ The base install (`pipx install tokenjam`) does NOT pull torch. Trim shows up in
29
29
  `tj optimize` analyzer choices regardless, but running it without the
30
30
  extra prints a clear install hint and exits.
31
31
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tokenjam"
7
- version = "0.4.2"
7
+ version = "0.5.1"
8
8
  description = "TokenJam — local-first OTel-native observability for Autonomous AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenjam/sdk",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "TypeScript SDK for TokenJam — local-first observability for AI agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,191 @@
1
+ """Endpoint tests for the Analytics pivot explorer (#210).
2
+
3
+ Covers the generalized group-by: metric × group_by × stack_by × filters → a
4
+ grouped series + KPI totals + the plan-tier framing block. The explorer's
5
+ line / bar / hbar views are all client-side pivots of this one `rows` shape.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import timedelta
10
+
11
+ import httpx
12
+ import pytest
13
+
14
+ from tokenjam.api.app import create_app
15
+ from tokenjam.core.config import ProviderBudget, TjConfig
16
+ from tokenjam.core.db import InMemoryBackend
17
+ from tokenjam.core.ingest import IngestPipeline
18
+ from tokenjam.utils.time_parse import utcnow
19
+ from tests.factories import make_llm_span, make_session, make_tool_span
20
+
21
+
22
+ def _app(db, config):
23
+ return create_app(config=config, db=db, ingest_pipeline=IngestPipeline(db=db, config=config))
24
+
25
+
26
+ def _seed(db, plan="api"):
27
+ now = utcnow()
28
+ for d in range(4):
29
+ for model, provider, cost in [("claude-opus-4-7", "anthropic", 0.06),
30
+ ("gpt-4o", "openai", 0.03)]:
31
+ sid = f"s{d}-{model}"
32
+ db.upsert_session(make_session(session_id=sid, plan_tier=plan, agent_id="cc"))
33
+ llm = make_llm_span(
34
+ session_id=sid, agent_id="cc", model=model, provider=provider,
35
+ input_tokens=2000, output_tokens=300, cost_usd=cost,
36
+ start_time=now - timedelta(days=d),
37
+ )
38
+ db.insert_span(llm)
39
+ for tool in ["Read", "Bash", "Grep"]:
40
+ t = make_tool_span(agent_id="cc", tool_name=tool, trace_id=llm.trace_id)
41
+ t.session_id = sid
42
+ t.start_time = now - timedelta(days=d)
43
+ db.insert_span(t)
44
+
45
+
46
+ async def _get(db, cfg, qs):
47
+ transport = httpx.ASGITransport(app=_app(db, cfg))
48
+ async with httpx.AsyncClient(transport=transport, base_url="http://t") as c:
49
+ return (await c.get("/api/v1/analytics?" + qs)).json()
50
+
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_spend_by_model_grouped_series_and_kpis():
54
+ db = InMemoryBackend()
55
+ cfg = TjConfig(version="1")
56
+ _seed(db)
57
+ d = await _get(db, cfg, "metric=spend&group_by=model&since=30d")
58
+ assert d["metric"] == "spend" and d["value_unit"] == "usd"
59
+ # groups sorted by total desc; opus (0.06×4) > gpt (0.03×4)
60
+ assert d["groups"] == ["claude-opus-4-7", "gpt-4o"]
61
+ assert d["rows"], "expected grouped rows"
62
+ for r in d["rows"]:
63
+ assert {"bucket", "group", "stack", "value", "tokens"} <= set(r)
64
+ # KPIs are window totals, independent of group_by
65
+ assert d["kpis"]["spend"] == pytest.approx(0.36)
66
+ assert d["kpis"]["sessions"] == 8
67
+ assert d["framing"]["pricing_mode"] == "api"
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ @pytest.mark.parametrize("metric,unit", [
72
+ ("tokens", "tokens"), ("sessions", "count"), ("events", "count"),
73
+ ])
74
+ async def test_metric_dimension_matrix(metric, unit):
75
+ db = InMemoryBackend()
76
+ cfg = TjConfig(version="1")
77
+ _seed(db)
78
+ d = await _get(db, cfg, f"metric={metric}&group_by=agent&since=30d")
79
+ assert d["value_unit"] == unit
80
+ assert d["groups"] == ["cc"]
81
+ assert d["rows"]
82
+
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_tool_category_dimension_includes_tool_spans():
86
+ """tool / tool_category breakdowns must see tool spans (NULL model), which
87
+ the LLM-cost `model IS NOT NULL` gate would otherwise hide."""
88
+ db = InMemoryBackend()
89
+ cfg = TjConfig(version="1")
90
+ _seed(db)
91
+ d = await _get(db, cfg, "metric=events&group_by=tool_category&since=30d")
92
+ assert set(d["groups"]) == {"file", "shell", "search"} # Read/Edit, Bash, Grep
93
+ assert "(none)" not in d["groups"]
94
+ # KPI totals reflect the FULL window (LLM spend included), not the tool-only
95
+ # subtype gate the breakdown applies — so "events by tool" never zeroes Spend.
96
+ assert d["kpis"]["spend"] > 0
97
+ assert d["kpis"]["tokens"] > 0
98
+
99
+
100
+ @pytest.mark.asyncio
101
+ async def test_stack_by_returns_second_dimension():
102
+ db = InMemoryBackend()
103
+ cfg = TjConfig(version="1")
104
+ _seed(db)
105
+ d = await _get(db, cfg, "metric=spend&group_by=provider&stack_by=model&since=30d")
106
+ assert d["stack_by"] == "model"
107
+ assert set(d["groups"]) == {"anthropic", "openai"}
108
+ assert set(d["stacks"]) == {"claude-opus-4-7", "gpt-4o"}
109
+ assert any(r["stack"] for r in d["rows"])
110
+
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_filters_scope_the_window():
114
+ db = InMemoryBackend()
115
+ cfg = TjConfig(version="1")
116
+ _seed(db)
117
+ d = await _get(db, cfg, "metric=spend&group_by=model&provider=anthropic&since=30d")
118
+ assert d["groups"] == ["claude-opus-4-7"]
119
+ assert "gpt-4o" not in d["groups"]
120
+
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_subscription_framing_suppresses_dollars():
124
+ db = InMemoryBackend()
125
+ cfg = TjConfig(version="1", budgets={"anthropic": ProviderBudget(plan="max_20x")})
126
+ _seed(db, plan="max_20x")
127
+ d = await _get(db, cfg, "metric=spend&group_by=model&since=30d")
128
+ assert d["framing"]["pricing_mode"] == "subscription"
129
+ # token volume is present per row so the UI renders token-share, not $
130
+ assert all("tokens" in r for r in d["rows"])
131
+
132
+
133
+ @pytest.mark.asyncio
134
+ async def test_unknown_metric_and_dimension_rejected():
135
+ db = InMemoryBackend()
136
+ cfg = TjConfig(version="1")
137
+ transport = httpx.ASGITransport(app=_app(db, cfg))
138
+ async with httpx.AsyncClient(transport=transport, base_url="http://t") as c:
139
+ assert (await c.get("/api/v1/analytics?metric=bogus")).status_code == 400
140
+ assert (await c.get("/api/v1/analytics?group_by=bogus")).status_code == 400
141
+
142
+
143
+ @pytest.mark.asyncio
144
+ async def test_empty_window_is_safe():
145
+ db = InMemoryBackend()
146
+ cfg = TjConfig(version="1")
147
+ d = await _get(db, cfg, "metric=spend&group_by=model&since=7d")
148
+ assert d["rows"] == [] and d["groups"] == []
149
+ assert d["kpis"]["spend"] == 0
150
+ assert "framing" in d
151
+ # #217: the sparkline/delta fields are always present. With a window set,
152
+ # the delta keys exist but are null (no prior data → no divide-by-zero).
153
+ assert d["kpi_series"] == []
154
+ assert set(d["kpi_deltas"]) == {"spend", "tokens", "events", "sessions"}
155
+ assert all(v is None for v in d["kpi_deltas"].values())
156
+
157
+
158
+ @pytest.mark.asyncio
159
+ async def test_kpi_series_and_deltas_for_sparklines():
160
+ """#217: the explorer returns a per-bucket `kpi_series` (all four metrics)
161
+ and a period-over-period `kpi_deltas` vs the prior equal-length window —
162
+ both computed server-side so the UI never aggregates per-bucket in JS."""
163
+ db = InMemoryBackend()
164
+ cfg = TjConfig(version="1")
165
+ _seed(db) # 4 days of data, two models/day
166
+ d = await _get(db, cfg, "metric=spend&group_by=model&since=30d")
167
+ # Per-bucket series carries every KPI metric for each bucket.
168
+ assert d["kpi_series"], "expected a non-empty kpi_series"
169
+ for b in d["kpi_series"]:
170
+ assert {"bucket", "spend", "tokens", "events", "sessions"} <= set(b)
171
+ # Series spend reconciles with the window KPI total.
172
+ assert sum(b["spend"] for b in d["kpi_series"]) == pytest.approx(d["kpis"]["spend"])
173
+ # Prior window had no data (seed is the most recent 4 days) → prev=0 → null
174
+ # delta, never a divide-by-zero.
175
+ assert set(d["kpi_deltas"]) == {"spend", "tokens", "events", "sessions"}
176
+ assert d["kpi_deltas"]["spend"] is None
177
+ assert d["kpi_prev"]["spend"] == 0
178
+
179
+
180
+ @pytest.mark.asyncio
181
+ async def test_kpi_delta_non_null_when_prior_window_has_data():
182
+ """#217: a real period-over-period % when both the current and prior
183
+ equal-length windows carry data (the seed spans 4 consecutive days, so a
184
+ 2-day window's prior 2 days are non-empty)."""
185
+ db = InMemoryBackend()
186
+ cfg = TjConfig(version="1")
187
+ _seed(db)
188
+ d = await _get(db, cfg, "metric=spend&group_by=model&since=2d")
189
+ assert d["kpi_prev"]["spend"] > 0, "prior window should have seeded data"
190
+ assert d["kpi_deltas"]["spend"] is not None
191
+ assert isinstance(d["kpi_deltas"]["spend"], float)