tokenjam 0.3.3__tar.gz → 0.3.4__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 (272) hide show
  1. {tokenjam-0.3.3 → tokenjam-0.3.4}/CHANGELOG.md +5 -0
  2. {tokenjam-0.3.3 → tokenjam-0.3.4}/PKG-INFO +5 -3
  3. {tokenjam-0.3.3 → tokenjam-0.3.4}/README.md +4 -2
  4. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/claude-code-integration.md +1 -1
  5. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/installation.md +28 -2
  6. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/optimize/trim.md +1 -1
  7. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/python-sdk.md +1 -1
  8. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/openclaw/README.md +1 -1
  9. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/hallucination-drift/README.md +3 -3
  10. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/retry-loop/README.md +3 -3
  11. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/surprise-cost/README.md +3 -3
  12. {tokenjam-0.3.3 → tokenjam-0.3.4}/pyproject.toml +1 -1
  13. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/package.json +1 -1
  14. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/factories.py +2 -0
  15. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/test_db.py +3 -3
  16. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/test_full_pipeline.py +35 -1
  17. tokenjam-0.3.4/tests/synthetic/test_cost_tracking.py +261 -0
  18. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_cmd_tokenmaxx.py +13 -9
  19. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_cost.py +17 -3
  20. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_ingest_helicone.py +68 -0
  21. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_ingest_langfuse.py +67 -0
  22. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_ingest_otlp.py +22 -0
  23. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_logs_converter.py +13 -7
  24. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_spans_stats_repair.py +2 -1
  25. tokenjam-0.3.4/tests/unit/test_ui_offline.py +127 -0
  26. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/app.py +14 -0
  27. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/logs.py +9 -1
  28. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_tokenmaxx.py +7 -6
  29. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/cost.py +15 -4
  30. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/db.py +18 -4
  31. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/ingest_adapters/helicone.py +8 -0
  32. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/ingest_adapters/langfuse.py +9 -0
  33. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/models.py +4 -0
  34. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/otel/otlp_parsing.py +1 -0
  35. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/otel/provider.py +4 -0
  36. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/ui/index.html +27 -7
  37. tokenjam-0.3.4/tokenjam/ui/vendor/htm.js +1 -0
  38. tokenjam-0.3.4/tokenjam/ui/vendor/preact-hooks.js +2 -0
  39. tokenjam-0.3.4/tokenjam/ui/vendor/preact.js +2 -0
  40. tokenjam-0.3.3/tests/synthetic/test_cost_tracking.py +0 -140
  41. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/CODEOWNERS +0 -0
  42. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
  45. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/pull_request_template.md +0 -0
  46. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/workflows/ci.yml +0 -0
  47. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/workflows/publish-npm.yml +0 -0
  48. {tokenjam-0.3.3 → tokenjam-0.3.4}/.github/workflows/publish-pypi.yml +0 -0
  49. {tokenjam-0.3.3 → tokenjam-0.3.4}/.gitignore +0 -0
  50. {tokenjam-0.3.3 → tokenjam-0.3.4}/AGENTS.md +0 -0
  51. {tokenjam-0.3.3 → tokenjam-0.3.4}/CLAUDE.md +0 -0
  52. {tokenjam-0.3.3 → tokenjam-0.3.4}/CONTRIBUTING.md +0 -0
  53. {tokenjam-0.3.3 → tokenjam-0.3.4}/LICENSE +0 -0
  54. {tokenjam-0.3.3 → tokenjam-0.3.4}/Makefile +0 -0
  55. {tokenjam-0.3.3 → tokenjam-0.3.4}/SECURITY.md +0 -0
  56. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/alerts.md +0 -0
  57. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/architecture.md +0 -0
  58. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/backfill/helicone.md +0 -0
  59. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/backfill/langfuse.md +0 -0
  60. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/backfill/otlp.md +0 -0
  61. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/backfill/overview.md +0 -0
  62. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/cli-reference.md +0 -0
  63. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/configuration.md +0 -0
  64. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/export.md +0 -0
  65. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/framework-support.md +0 -0
  66. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/internal/specs/.gitkeep +0 -0
  67. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/nemoclaw-integration.md +0 -0
  68. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/openclaw.md +0 -0
  69. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/optimize/cache.md +0 -0
  70. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/optimize/downsize.md +0 -0
  71. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/optimize/script.md +0 -0
  72. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/policy/overview.md +0 -0
  73. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/screenshots/tj-alerts.png +0 -0
  74. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/screenshots/tj-budget.png +0 -0
  75. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/screenshots/tj-cost.png +0 -0
  76. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/screenshots/tj-status.png +0 -0
  77. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/screenshots/tj-traces.png +0 -0
  78. {tokenjam-0.3.3 → tokenjam-0.3.4}/docs/typescript-sdk.md +0 -0
  79. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/README.md +0 -0
  80. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/alerts_and_drift/_shared.py +0 -0
  81. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
  82. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/alerts_and_drift/drift_demo.py +0 -0
  83. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
  84. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/rag_pipeline.py +0 -0
  85. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/research_team.py +0 -0
  86. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/router_agent.py +0 -0
  87. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/sample_docs/agent_patterns.txt +0 -0
  88. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/sample_docs/cost_management.txt +0 -0
  89. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/sample_docs/observability.txt +0 -0
  90. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/multi/sample_docs/safety.txt +0 -0
  91. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_framework/autogen_agent.py +0 -0
  92. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_framework/crewai_agent.py +0 -0
  93. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_framework/langchain_agent.py +0 -0
  94. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_framework/langgraph_agent.py +0 -0
  95. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_framework/llamaindex_agent.py +0 -0
  96. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_provider/anthropic_agent.py +0 -0
  97. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_provider/bedrock_agent.py +0 -0
  98. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_provider/gemini_agent.py +0 -0
  99. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_provider/litellm_agent.py +0 -0
  100. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_provider/openai_agent.py +0 -0
  101. {tokenjam-0.3.3 → tokenjam-0.3.4}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
  102. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/hallucination-drift/BLOG.md +0 -0
  103. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/hallucination-drift/scenario.py +0 -0
  104. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/retry-loop/BLOG.md +0 -0
  105. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/retry-loop/scenario.py +0 -0
  106. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/surprise-cost/BLOG.md +0 -0
  107. {tokenjam-0.3.3 → tokenjam-0.3.4}/incidents/surprise-cost/scenario.py +0 -0
  108. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/README.md +0 -0
  109. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/package-lock.json +0 -0
  110. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/client.test.ts +0 -0
  111. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/client.ts +0 -0
  112. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/index.ts +0 -0
  113. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/semconv.test.ts +0 -0
  114. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/semconv.ts +0 -0
  115. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/span-builder.test.ts +0 -0
  116. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/span-builder.ts +0 -0
  117. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/src/types.ts +0 -0
  118. {tokenjam-0.3.3 → tokenjam-0.3.4}/sdk-ts/tsconfig.json +0 -0
  119. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/__init__.py +0 -0
  120. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/__init__.py +0 -0
  121. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/email_agent_budget_breach.py +0 -0
  122. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/email_agent_drift.py +0 -0
  123. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/email_agent_loop.py +0 -0
  124. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/email_agent_normal.py +0 -0
  125. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/mock_llm.py +0 -0
  126. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/agents/test_mock_scenarios.py +0 -0
  127. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/conftest.py +0 -0
  128. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/e2e/__init__.py +0 -0
  129. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/e2e/conftest.py +0 -0
  130. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/e2e/test_real_llm.py +0 -0
  131. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/fixtures/helicone_real_response.json +0 -0
  132. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/fixtures/langfuse_real_response.json +0 -0
  133. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/fixtures/otlp_sample.json +0 -0
  134. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/__init__.py +0 -0
  135. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/test_api.py +0 -0
  136. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/test_cli.py +0 -0
  137. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/test_demos.py +0 -0
  138. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/integration/test_logs_api.py +0 -0
  139. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/manual-new-release-tests.md +0 -0
  140. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/manual-pre-release-testing.md +0 -0
  141. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/synthetic/__init__.py +0 -0
  142. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/synthetic/test_alert_rules.py +0 -0
  143. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/synthetic/test_drift_detection.py +0 -0
  144. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/synthetic/test_ingest.py +0 -0
  145. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/synthetic/test_schema_validation.py +0 -0
  146. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/toy_agent/toy_agent.py +0 -0
  147. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/__init__.py +0 -0
  148. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_alerts.py +0 -0
  149. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_backfill.py +0 -0
  150. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_cache_efficacy.py +0 -0
  151. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_cache_recommend.py +0 -0
  152. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_cmd_policy.py +0 -0
  153. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_cmd_stop.py +0 -0
  154. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_compare.py +0 -0
  155. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_config.py +0 -0
  156. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_config_secret_divergence.py +0 -0
  157. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_demo_env.py +0 -0
  158. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_demo_scenarios.py +0 -0
  159. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_drift.py +0 -0
  160. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_export_claude_code.py +0 -0
  161. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_formatting.py +0 -0
  162. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_litellm_client.py +0 -0
  163. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_litellm_integration.py +0 -0
  164. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_mcp_server.py +0 -0
  165. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_models.py +0 -0
  166. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_onboard_codex.py +0 -0
  167. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_onboard_daemon.py +0 -0
  168. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_openclaw_ingest.py +0 -0
  169. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_optimize.py +0 -0
  170. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_pricing_override.py +0 -0
  171. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_prompt_bloat.py +0 -0
  172. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_time_parse.py +0 -0
  173. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_transport_401.py +0 -0
  174. {tokenjam-0.3.3 → tokenjam-0.3.4}/tests/unit/test_workflow_restructure.py +0 -0
  175. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/__init__.py +0 -0
  176. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/__init__.py +0 -0
  177. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/deps.py +0 -0
  178. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/middleware.py +0 -0
  179. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/__init__.py +0 -0
  180. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/agents.py +0 -0
  181. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/alerts.py +0 -0
  182. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/budget.py +0 -0
  183. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/cost.py +0 -0
  184. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/cost_compare.py +0 -0
  185. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/drift.py +0 -0
  186. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/metrics.py +0 -0
  187. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/optimize.py +0 -0
  188. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/otlp.py +0 -0
  189. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/spans.py +0 -0
  190. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/status.py +0 -0
  191. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/tools.py +0 -0
  192. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/api/routes/traces.py +0 -0
  193. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/__init__.py +0 -0
  194. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_alerts.py +0 -0
  195. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_backfill.py +0 -0
  196. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_budget.py +0 -0
  197. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_cost.py +0 -0
  198. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_demo.py +0 -0
  199. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_doctor.py +0 -0
  200. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_drift.py +0 -0
  201. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_export.py +0 -0
  202. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_mcp.py +0 -0
  203. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_onboard.py +0 -0
  204. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_optimize.py +0 -0
  205. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_policy.py +0 -0
  206. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_report.py +0 -0
  207. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_serve.py +0 -0
  208. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_status.py +0 -0
  209. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_stop.py +0 -0
  210. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_tools.py +0 -0
  211. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_traces.py +0 -0
  212. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/cmd_uninstall.py +0 -0
  213. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/cli/main.py +0 -0
  214. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/__init__.py +0 -0
  215. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/alerts.py +0 -0
  216. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/api_backend.py +0 -0
  217. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/backfill.py +0 -0
  218. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/config.py +0 -0
  219. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/drift.py +0 -0
  220. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/export/__init__.py +0 -0
  221. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/export/claude_code.py +0 -0
  222. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/ingest.py +0 -0
  223. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/ingest_adapters/__init__.py +0 -0
  224. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/ingest_adapters/otlp.py +0 -0
  225. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/README.md +0 -0
  226. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/__init__.py +0 -0
  227. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
  228. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
  229. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
  230. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
  231. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/model_downgrade.py +0 -0
  232. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
  233. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/analyzers/workflow_restructure.py +0 -0
  234. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/registry.py +0 -0
  235. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/runner.py +0 -0
  236. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/optimize/types.py +0 -0
  237. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/pricing.py +0 -0
  238. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/retention.py +0 -0
  239. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/core/schema_validator.py +0 -0
  240. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/demo/__init__.py +0 -0
  241. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/demo/env.py +0 -0
  242. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/mcp/__init__.py +0 -0
  243. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/mcp/server.py +0 -0
  244. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/otel/__init__.py +0 -0
  245. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/otel/exporters.py +0 -0
  246. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/otel/semconv.py +0 -0
  247. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/pricing/models.toml +0 -0
  248. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/py.typed +0 -0
  249. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/__init__.py +0 -0
  250. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/agent.py +0 -0
  251. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/bootstrap.py +0 -0
  252. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/client.py +0 -0
  253. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/http_exporter.py +0 -0
  254. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/__init__.py +0 -0
  255. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/anthropic.py +0 -0
  256. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/autogen.py +0 -0
  257. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/base.py +0 -0
  258. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/bedrock.py +0 -0
  259. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/crewai.py +0 -0
  260. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/gemini.py +0 -0
  261. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/langchain.py +0 -0
  262. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/langgraph.py +0 -0
  263. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/litellm.py +0 -0
  264. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/llamaindex.py +0 -0
  265. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
  266. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/openai.py +0 -0
  267. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
  268. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/sdk/transport.py +0 -0
  269. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/utils/__init__.py +0 -0
  270. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/utils/formatting.py +0 -0
  271. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/utils/ids.py +0 -0
  272. {tokenjam-0.3.3 → tokenjam-0.3.4}/tokenjam/utils/time_parse.py +0 -0
@@ -54,6 +54,11 @@ This release pivots TokenJam toward cost-optimization for autonomous agents. Fou
54
54
  - **`tj status` surfaces unknown plan tiers.** When sessions exist with `plan_tier = 'unknown'`, prints a one-line note pointing the user at `tj onboard --reconfigure`. Exit code unchanged.
55
55
  - **`tj optimize` plan-tier-aware rendering.** When every session in the window has `plan_tier = 'unknown'`, dollar figures are suppressed and a header note explains why. Mixed / partial-unknown windows render normally with an advisory note.
56
56
  - **MCP `get_optimize_report` tool.** Now accepts `findings: list[str]` (was `only: str`). Docstring surfaces for both API-billing and subscription-plan-efficiency phrasings.
57
+ - **`tj tokenmaxx` tier ladder expanded to six tiers and renamed.** Two highest tiers renamed from `TokenChad` / `TokenGigaChad` to `TokenSuperMaxxer` / `TokenGigaMaxxer`, and a new `TokenMegaMaxxer` tier slots between them covering the 20× – 50× multiplier range. The previous top tier started at 20×; the new top tier starts at 50×, so the absolute headline for very-heavy users is more meaningful. Fire-emoji escalation matches the new tier count: 🔥 → 🔥🔥 → 🔥🔥🔥. The quip that previously belonged to `TokenGigaChad` ("Touch grass. Then run `tj optimize`.") now belongs to `TokenMegaMaxxer`; `TokenGigaMaxxer` gets its own escalated quip. JSON output's `tier` field carries the new label string verbatim; consumers reading the `tier` value must update accordingly.
58
+
59
+ ### Fixed
60
+ - **Cache-only spans were costed at $0.** A prompt-cache hit (0 new input/output tokens but non-zero cache-read tokens) bills the cache-read rate, but both `calculate_cost` and `CostEngine.process_span` short-circuited on input/output alone, dropping the span as a no-op and under-reporting spend. The early-return guards now fire only when *all* token counts are zero, so cache-read (and cache-write) costs are charged correctly.
61
+ - **Cache-write (cache-creation) tokens were dropped on the live ingest path.** The SDK integrations emit `gen_ai.usage.cache_creation_tokens`, the pricing table carries a `cache_write_per_mtok` rate, and `calculate_cost` already priced it — but the OTLP span parser and provider reader only read cache-read tokens, so cache-creation tokens never reached `CostEngine.process_span` and their (higher-rate) cost was never charged. `NormalizedSpan` now carries `cache_write_tokens`, both parsers populate it, and `process_span` charges it. Only the backfill path costed cache-write before; the live path now matches.
57
62
 
58
63
  ### Internal
59
64
  - **Registry-driven optimize analyzers.** `tokenjam/core/optimize.py` split into `tokenjam/core/optimize/` package with `registry.py`, `runner.py`, `types.py`, and `analyzers/` subpackage using `pkgutil` auto-discovery. New analyzers drop a file under `analyzers/` with a `@register("name")` decorator — nothing else needs editing. See `tokenjam/core/optimize/README.md`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenjam
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -74,9 +74,11 @@ TokenJam reads your agent's telemetry and tells you when to downsize, when to tr
74
74
  [![OTel](https://img.shields.io/badge/OTel-GenAI%20SemConv-3d8eff?labelColor=0d1117)](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
75
75
 
76
76
  ```
77
- pip install tokenjam
77
+ pipx install tokenjam
78
78
  ```
79
79
 
80
+ <sub>Don't have pipx? `brew install pipx` on macOS, `apt install pipx` on Debian/Ubuntu, or see [docs/installation.md](docs/installation.md). `pip install tokenjam` also works in a clean venv.</sub>
81
+
80
82
  **No cloud · No signup · No vendor lock-in**
81
83
 
82
84
  </div>
@@ -147,7 +149,7 @@ Run all four with `tj optimize`. Run several with `tj optimize downsize cache tr
147
149
  For **Claude Code** users — zero code, auto-backfills your last 30 days:
148
150
 
149
151
  ```bash
150
- pip install "tokenjam[mcp]"
152
+ pipx install 'tokenjam[mcp]'
151
153
  tj onboard --claude-code
152
154
  tj optimize # cost-saving candidates from your actual usage
153
155
  ```
@@ -16,9 +16,11 @@ TokenJam reads your agent's telemetry and tells you when to downsize, when to tr
16
16
  [![OTel](https://img.shields.io/badge/OTel-GenAI%20SemConv-3d8eff?labelColor=0d1117)](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
17
17
 
18
18
  ```
19
- pip install tokenjam
19
+ pipx install tokenjam
20
20
  ```
21
21
 
22
+ <sub>Don't have pipx? `brew install pipx` on macOS, `apt install pipx` on Debian/Ubuntu, or see [docs/installation.md](docs/installation.md). `pip install tokenjam` also works in a clean venv.</sub>
23
+
22
24
  **No cloud · No signup · No vendor lock-in**
23
25
 
24
26
  </div>
@@ -89,7 +91,7 @@ Run all four with `tj optimize`. Run several with `tj optimize downsize cache tr
89
91
  For **Claude Code** users — zero code, auto-backfills your last 30 days:
90
92
 
91
93
  ```bash
92
- pip install "tokenjam[mcp]"
94
+ pipx install 'tokenjam[mcp]'
93
95
  tj onboard --claude-code
94
96
  tj optimize # cost-saving candidates from your actual usage
95
97
  ```
@@ -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
- pip install "tokenjam[mcp]"
6
+ pipx install 'tokenjam[mcp]'
7
7
  tj onboard --claude-code
8
8
  # Restart Claude Code, then:
9
9
  tj status --agent claude-code-<project>
@@ -5,10 +5,36 @@ TokenJam ships as a Python package on PyPI and a TypeScript SDK on npm. Pick the
5
5
  ## Base install
6
6
 
7
7
  ```bash
8
+ pipx install tokenjam
9
+ ```
10
+
11
+ This is the recommended install path on **all platforms**. `pipx` automatically creates an isolated venv for the `tj` CLI, which means:
12
+
13
+ - It works on macOS with Homebrew Python (which refuses `pip install` into its managed environment by default — [PEP 668](https://peps.python.org/pep-0668/)).
14
+ - It works on Debian 12+ / Ubuntu 24+ (same PEP 668 enforcement).
15
+ - It doesn't pollute your system Python or any project's venv.
16
+
17
+ Don't have `pipx`? Install it with one of:
18
+
19
+ | Platform | Command |
20
+ |---|---|
21
+ | macOS | `brew install pipx` |
22
+ | Debian / Ubuntu | `apt install pipx` |
23
+ | Windows | `py -m pip install --user pipx` |
24
+ | Anywhere else | `python3 -m pip install --user pipx` |
25
+
26
+ Then ensure pipx's bin dir is on your `PATH` with `pipx ensurepath`.
27
+
28
+ ### Alternative: pip in a venv
29
+
30
+ If you prefer plain pip (or need to install into an existing project venv):
31
+
32
+ ```bash
33
+ python3 -m venv .venv && source .venv/bin/activate
8
34
  pip install tokenjam
9
35
  ```
10
36
 
11
- This is enough for the CLI (`tj`), local REST API (`tj serve`), the four out-of-box optimize analyzers that don't need ML models, and every native SDK integration except LLMLingua-based Trim. Requires Python ≥ 3.10.
37
+ Either path is enough for the CLI (`tj`), local REST API (`tj serve`), the four out-of-box optimize analyzers that don't need ML models, and every native SDK integration except LLMLingua-based Trim. Requires Python ≥ 3.10.
12
38
 
13
39
  After install, run:
14
40
 
@@ -37,7 +63,7 @@ TokenJam keeps heavyweight ML dependencies, framework adapters, and the MCP serv
37
63
  Combine multiple extras:
38
64
 
39
65
  ```bash
40
- pip install "tokenjam[mcp,bloat]"
66
+ pipx install 'tokenjam[mcp,bloat]'
41
67
  ```
42
68
 
43
69
  ### Bloat extra details
@@ -22,7 +22,7 @@ LLMLingua-2 pulls in PyTorch and transformers (~2GB). Kept out of the
22
22
  base install:
23
23
 
24
24
  ```bash
25
- pip install "tokenjam[bloat]"
25
+ pipx install 'tokenjam[bloat]'
26
26
  ```
27
27
 
28
28
  The base `pip install tokenjam` does NOT pull torch. Trim shows up in
@@ -5,7 +5,7 @@ For any Python agent — Anthropic, OpenAI, Gemini, Bedrock, LangChain, CrewAI,
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- pip install tokenjam
8
+ pipx install tokenjam
9
9
  tj onboard # creates config, generates ingest secret
10
10
  tj doctor # verify your setup
11
11
  ```
@@ -5,7 +5,7 @@ This is a config-only integration — no Python code needed. OpenClaw's built-in
5
5
  ## Step 1: Start TokenJam
6
6
 
7
7
  ```bash
8
- pip install tokenjam
8
+ pipx install tokenjam
9
9
  tj onboard
10
10
  tj serve &
11
11
  ```
@@ -1,6 +1,6 @@
1
1
  # My agent worked yesterday. Today it's possessed.
2
2
 
3
- **Run it:** `pip install tokenjam && tj demo hallucination-drift`
3
+ **Run it:** `pipx install tokenjam && tj demo hallucination-drift`
4
4
 
5
5
  ---
6
6
 
@@ -60,7 +60,7 @@ The demo uses `baseline_sessions = 5` for speed. In production, 10–50 sessions
60
60
  ## Try it yourself
61
61
 
62
62
  ```bash
63
- pip install tokenjam
63
+ pipx install tokenjam
64
64
  tj demo hallucination-drift
65
65
  ```
66
66
 
@@ -75,4 +75,4 @@ To track drift on your real agent, wire up the TokenJam SDK, enable drift in `tj
75
75
 
76
76
  ---
77
77
 
78
- [TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does.
78
+ [TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pipx install tokenjam` and start seeing what your agent actually does.
@@ -1,6 +1,6 @@
1
1
  # Your agent isn't flaky. You're blind.
2
2
 
3
- **Run it:** `pip install tokenjam && tj demo retry-loop`
3
+ **Run it:** `pipx install tokenjam && tj demo retry-loop`
4
4
 
5
5
  ---
6
6
 
@@ -57,7 +57,7 @@ The loop was visible from span #4. Your logs didn't surface it until a user comp
57
57
  ## Try it yourself
58
58
 
59
59
  ```bash
60
- pip install tokenjam
60
+ pipx install tokenjam
61
61
  tj demo retry-loop
62
62
  ```
63
63
 
@@ -72,4 +72,4 @@ To catch this in your real agent, wire up the TokenJam SDK (`@watch()` + `patch_
72
72
 
73
73
  ---
74
74
 
75
- [TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does.
75
+ [TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pipx install tokenjam` and start seeing what your agent actually does.
@@ -1,6 +1,6 @@
1
1
  # Why did my agent just spend $47 on a hello world?
2
2
 
3
- **Run it:** `pip install tokenjam && tj demo surprise-cost`
3
+ **Run it:** `pipx install tokenjam && tj demo surprise-cost`
4
4
 
5
5
  ---
6
6
 
@@ -75,7 +75,7 @@ TokenJam fires `cost_budget_session` and `cost_budget_daily` alerts when limits
75
75
  ## Try it yourself
76
76
 
77
77
  ```bash
78
- pip install tokenjam
78
+ pipx install tokenjam
79
79
  tj demo surprise-cost
80
80
  ```
81
81
 
@@ -90,4 +90,4 @@ To track real spend, instrument your agent with the tokenjam SDK and run `tj ser
90
90
 
91
91
  ---
92
92
 
93
- [TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does.
93
+ [TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pipx install tokenjam` and start seeing what your agent actually does.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tokenjam"
7
- version = "0.3.3"
7
+ version = "0.3.4"
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.3.3",
3
+ "version": "0.3.4",
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",
@@ -19,6 +19,7 @@ def make_llm_span(
19
19
  input_tokens: int = 1000,
20
20
  output_tokens: int = 200,
21
21
  cache_tokens: int = 0,
22
+ cache_write_tokens: int = 0,
22
23
  cost_usd: float | None = None,
23
24
  tool_name: str | None = None,
24
25
  status: str = "ok",
@@ -59,6 +60,7 @@ def make_llm_span(
59
60
  input_tokens=input_tokens,
60
61
  output_tokens=output_tokens,
61
62
  cache_tokens=cache_tokens,
63
+ cache_write_tokens=cache_write_tokens,
62
64
  cost_usd=cost_usd,
63
65
  conversation_id=conversation_id,
64
66
  attributes=attrs,
@@ -42,8 +42,8 @@ def _insert_agent(db, agent_id="test-agent"):
42
42
  def test_migrations_run_on_empty_db():
43
43
  backend = InMemoryBackend()
44
44
  rows = backend.conn.execute("SELECT version FROM schema_migrations").fetchall()
45
- assert len(rows) == 4
46
- assert {r[0] for r in rows} == {1, 2, 3, 4}
45
+ assert len(rows) == 5
46
+ assert {r[0] for r in rows} == {1, 2, 3, 4, 5}
47
47
  backend.close()
48
48
 
49
49
 
@@ -52,7 +52,7 @@ def test_migrations_are_idempotent():
52
52
  # Running migrations again should not raise
53
53
  run_migrations(backend.conn)
54
54
  rows = backend.conn.execute("SELECT version FROM schema_migrations").fetchall()
55
- assert len(rows) == 4
55
+ assert len(rows) == 5
56
56
  backend.close()
57
57
 
58
58
 
@@ -34,7 +34,7 @@ from tokenjam.core.db import InMemoryBackend
34
34
  from tokenjam.core.ingest import IngestPipeline
35
35
  from tokenjam.core.models import AgentRecord, NormalizedSpan, SpanKind, SpanStatus
36
36
  from tokenjam.core.schema_validator import SchemaValidator
37
- from tokenjam.otel.provider import TjSpanExporter
37
+ from tokenjam.otel.provider import TjSpanExporter, convert_otel_span
38
38
  from tokenjam.otel.semconv import GenAIAttributes
39
39
  from tokenjam.sdk.agent import watch, AgentSession, record_llm_call, record_tool_call
40
40
  from tokenjam.utils.time_parse import utcnow
@@ -159,6 +159,40 @@ def full_stack():
159
159
  db.close()
160
160
 
161
161
 
162
+ # ── OTel ReadableSpan -> NormalizedSpan ──────────────────────────────────
163
+
164
+
165
+ def test_convert_otel_span_extracts_cache_read_and_write_tokens():
166
+ """convert_otel_span indexes both cache-read and cache-creation tokens.
167
+
168
+ Regression: provider previously read only CACHE_READ_TOKENS, dropping
169
+ cache-creation tokens so cache-write cost was never charged on this path.
170
+ """
171
+ collected: list[ReadableSpan] = []
172
+
173
+ class _Collector(SpanExporter):
174
+ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
175
+ collected.extend(spans)
176
+ return SpanExportResult.SUCCESS
177
+
178
+ def shutdown(self) -> None:
179
+ pass
180
+
181
+ provider = TracerProvider()
182
+ provider.add_span_processor(SimpleSpanProcessor(_Collector()))
183
+ tracer = provider.get_tracer("test")
184
+
185
+ with tracer.start_as_current_span("gen_ai.llm.call") as span:
186
+ span.set_attribute(GenAIAttributes.REQUEST_MODEL, "claude-haiku-4-5")
187
+ span.set_attribute(GenAIAttributes.CACHE_READ_TOKENS, 1000)
188
+ span.set_attribute(GenAIAttributes.CACHE_CREATE_TOKENS, 2000)
189
+
190
+ assert len(collected) == 1
191
+ normalized = convert_otel_span(collected[0])
192
+ assert normalized.cache_tokens == 1000
193
+ assert normalized.cache_write_tokens == 2000
194
+
195
+
162
196
  # ── SDK -> Pipeline -> DB ─────────────────────────────────────────────────
163
197
 
164
198
 
@@ -0,0 +1,261 @@
1
+ """Synthetic tests for CostEngine with an in-memory DuckDB backend."""
2
+ from __future__ import annotations
3
+
4
+ import duckdb
5
+ import pytest
6
+
7
+ from tokenjam.core.cost import CostEngine
8
+ from tests.factories import make_llm_span, make_session
9
+
10
+
11
+ class FakeDB:
12
+ """Minimal DB stub with just enough schema for CostEngine tests."""
13
+
14
+ def __init__(self) -> None:
15
+ self.conn = duckdb.connect(":memory:")
16
+ self.conn.execute("""
17
+ CREATE TABLE spans (
18
+ span_id TEXT PRIMARY KEY,
19
+ cost_usd DOUBLE
20
+ )
21
+ """)
22
+ self.conn.execute("""
23
+ CREATE TABLE sessions (
24
+ session_id TEXT PRIMARY KEY,
25
+ total_cost_usd DOUBLE
26
+ )
27
+ """)
28
+
29
+ def insert_span_stub(self, span_id: str) -> None:
30
+ self.conn.execute(
31
+ "INSERT INTO spans (span_id, cost_usd) VALUES (?, NULL)",
32
+ [span_id],
33
+ )
34
+
35
+ def insert_session_stub(self, session_id: str) -> None:
36
+ self.conn.execute(
37
+ "INSERT INTO sessions (session_id, total_cost_usd) VALUES (?, NULL)",
38
+ [session_id],
39
+ )
40
+
41
+ def get_span_cost(self, span_id: str) -> float | None:
42
+ row = self.conn.execute(
43
+ "SELECT cost_usd FROM spans WHERE span_id = ?", [span_id]
44
+ ).fetchone()
45
+ return row[0] if row else None
46
+
47
+ def get_session_cost(self, session_id: str) -> float | None:
48
+ row = self.conn.execute(
49
+ "SELECT total_cost_usd FROM sessions WHERE session_id = ?", [session_id]
50
+ ).fetchone()
51
+ return row[0] if row else None
52
+
53
+
54
+ @pytest.fixture
55
+ def fake_db() -> FakeDB:
56
+ return FakeDB()
57
+
58
+
59
+ @pytest.fixture
60
+ def engine(fake_db: FakeDB) -> CostEngine:
61
+ return CostEngine(fake_db)
62
+
63
+
64
+ def test_cost_engine_updates_span_cost_in_db(fake_db: FakeDB, engine: CostEngine) -> None:
65
+ span = make_llm_span(
66
+ provider="anthropic", model="claude-haiku-4-5",
67
+ input_tokens=1000, output_tokens=200,
68
+ )
69
+ fake_db.insert_span_stub(span.span_id)
70
+
71
+ engine.process_span(span)
72
+
73
+ db_cost = fake_db.get_span_cost(span.span_id)
74
+ assert db_cost is not None
75
+ assert db_cost == pytest.approx(0.0016)
76
+ assert span.cost_usd == pytest.approx(0.0016)
77
+
78
+
79
+ def test_cost_engine_updates_session_total_cost(fake_db: FakeDB, engine: CostEngine) -> None:
80
+ session = make_session()
81
+ fake_db.insert_session_stub(session.session_id)
82
+
83
+ span = make_llm_span(
84
+ provider="anthropic", model="claude-haiku-4-5",
85
+ input_tokens=1000, output_tokens=200,
86
+ session_id=session.session_id,
87
+ )
88
+ fake_db.insert_span_stub(span.span_id)
89
+
90
+ engine.process_span(span)
91
+
92
+ session_cost = fake_db.get_session_cost(session.session_id)
93
+ assert session_cost is not None
94
+ assert session_cost == pytest.approx(0.0016)
95
+
96
+
97
+ def test_cost_engine_accumulates_across_multiple_spans(
98
+ fake_db: FakeDB, engine: CostEngine,
99
+ ) -> None:
100
+ session = make_session()
101
+ fake_db.insert_session_stub(session.session_id)
102
+
103
+ for _ in range(3):
104
+ span = make_llm_span(
105
+ provider="anthropic", model="claude-haiku-4-5",
106
+ input_tokens=1000, output_tokens=200,
107
+ session_id=session.session_id,
108
+ )
109
+ fake_db.insert_span_stub(span.span_id)
110
+ engine.process_span(span)
111
+
112
+ session_cost = fake_db.get_session_cost(session.session_id)
113
+ assert session_cost is not None
114
+ # 3 * 0.0016 = 0.0048
115
+ assert session_cost == pytest.approx(0.0048)
116
+
117
+
118
+ def test_cost_engine_no_op_when_tokens_missing(fake_db: FakeDB, engine: CostEngine) -> None:
119
+ span = make_llm_span(
120
+ provider="anthropic", model="claude-haiku-4-5",
121
+ input_tokens=0, output_tokens=0,
122
+ )
123
+ fake_db.insert_span_stub(span.span_id)
124
+
125
+ engine.process_span(span)
126
+
127
+ db_cost = fake_db.get_span_cost(span.span_id)
128
+ assert db_cost is None
129
+ assert span.cost_usd is None
130
+
131
+
132
+ def test_cost_engine_costs_cache_only_span(fake_db: FakeDB, engine: CostEngine) -> None:
133
+ # A span with no new input/output but cache-read tokens (a cache hit) still
134
+ # costs the cache-read rate and must be recorded, not dropped as a no-op.
135
+ # claude-haiku-4-5: cache_read=0.08 per MTok.
136
+ span = make_llm_span(
137
+ provider="anthropic", model="claude-haiku-4-5",
138
+ input_tokens=0, output_tokens=0, cache_tokens=1_000_000,
139
+ )
140
+ fake_db.insert_span_stub(span.span_id)
141
+
142
+ engine.process_span(span)
143
+
144
+ db_cost = fake_db.get_span_cost(span.span_id)
145
+ assert db_cost == pytest.approx(0.08)
146
+ assert span.cost_usd == pytest.approx(0.08)
147
+
148
+
149
+ def test_cost_engine_cache_only_span_updates_session_total(
150
+ fake_db: FakeDB, engine: CostEngine,
151
+ ) -> None:
152
+ # The cache-only span's cost must also accumulate into the session total,
153
+ # not just the span row — dropping it previously under-reported the session.
154
+ session = make_session()
155
+ fake_db.insert_session_stub(session.session_id)
156
+
157
+ span = make_llm_span(
158
+ provider="anthropic", model="claude-haiku-4-5",
159
+ input_tokens=0, output_tokens=0, cache_tokens=1_000_000,
160
+ session_id=session.session_id,
161
+ )
162
+ fake_db.insert_span_stub(span.span_id)
163
+
164
+ engine.process_span(span)
165
+
166
+ session_cost = fake_db.get_session_cost(session.session_id)
167
+ assert session_cost == pytest.approx(0.08)
168
+
169
+
170
+ def test_cost_engine_costs_cache_write_span(fake_db: FakeDB, engine: CostEngine) -> None:
171
+ # A span whose only tokens are cache-CREATION (cache write) must be costed at
172
+ # the cache-write rate, not dropped as a no-op and not charged the read rate.
173
+ # claude-haiku-4-5: cache_write=1.00 per MTok.
174
+ span = make_llm_span(
175
+ provider="anthropic", model="claude-haiku-4-5",
176
+ input_tokens=0, output_tokens=0, cache_write_tokens=1_000_000,
177
+ )
178
+ fake_db.insert_span_stub(span.span_id)
179
+
180
+ engine.process_span(span)
181
+
182
+ db_cost = fake_db.get_span_cost(span.span_id)
183
+ assert db_cost == pytest.approx(1.00)
184
+ assert span.cost_usd == pytest.approx(1.00)
185
+
186
+
187
+ def test_cost_engine_costs_cache_read_and_write_together(
188
+ fake_db: FakeDB, engine: CostEngine,
189
+ ) -> None:
190
+ # Read and write cache tokens are priced at different rates and must both be
191
+ # charged. claude-haiku-4-5: cache_read=0.08, cache_write=1.00 per MTok.
192
+ span = make_llm_span(
193
+ provider="anthropic", model="claude-haiku-4-5",
194
+ input_tokens=0, output_tokens=0,
195
+ cache_tokens=1_000_000, cache_write_tokens=1_000_000,
196
+ )
197
+ fake_db.insert_span_stub(span.span_id)
198
+
199
+ engine.process_span(span)
200
+
201
+ db_cost = fake_db.get_span_cost(span.span_id)
202
+ assert db_cost == pytest.approx(1.08)
203
+ assert span.cost_usd == pytest.approx(1.08)
204
+
205
+
206
+ def test_cost_engine_cache_write_span_updates_session_total(
207
+ fake_db: FakeDB, engine: CostEngine,
208
+ ) -> None:
209
+ # The cache-write span's cost must also accumulate into the session total.
210
+ session = make_session()
211
+ fake_db.insert_session_stub(session.session_id)
212
+
213
+ span = make_llm_span(
214
+ provider="anthropic", model="claude-haiku-4-5",
215
+ input_tokens=0, output_tokens=0, cache_write_tokens=1_000_000,
216
+ session_id=session.session_id,
217
+ )
218
+ fake_db.insert_span_stub(span.span_id)
219
+
220
+ engine.process_span(span)
221
+
222
+ session_cost = fake_db.get_session_cost(session.session_id)
223
+ assert session_cost == pytest.approx(1.00)
224
+
225
+
226
+ def test_cost_engine_cache_write_pre_priced_does_not_double_count_session(
227
+ fake_db: FakeDB, engine: CostEngine,
228
+ ) -> None:
229
+ # A pre-priced span (cost_usd already set, e.g. from the parser) has its
230
+ # session cost handled by ingest's _build_or_update_session. process_span
231
+ # must still recompute the span cost but must NOT re-add to the session
232
+ # total, or cache-write spend would be double-counted.
233
+ session = make_session()
234
+ fake_db.conn.execute(
235
+ "INSERT INTO sessions (session_id, total_cost_usd) VALUES (?, ?)",
236
+ [session.session_id, 5.0],
237
+ )
238
+
239
+ span = make_llm_span(
240
+ provider="anthropic", model="claude-haiku-4-5",
241
+ input_tokens=0, output_tokens=0, cache_write_tokens=1_000_000,
242
+ session_id=session.session_id, cost_usd=1.00,
243
+ )
244
+ fake_db.insert_span_stub(span.span_id)
245
+
246
+ engine.process_span(span)
247
+
248
+ # Span cost recomputed, session total left untouched (no double-count).
249
+ assert fake_db.get_span_cost(span.span_id) == pytest.approx(1.00)
250
+ assert fake_db.get_session_cost(session.session_id) == pytest.approx(5.0)
251
+
252
+
253
+ def test_cost_engine_no_op_when_provider_missing(fake_db: FakeDB, engine: CostEngine) -> None:
254
+ span = make_llm_span(input_tokens=1000, output_tokens=200)
255
+ span.provider = None
256
+ fake_db.insert_span_stub(span.span_id)
257
+
258
+ engine.process_span(span)
259
+
260
+ db_cost = fake_db.get_span_cost(span.span_id)
261
+ assert db_cost is None
@@ -21,17 +21,19 @@ def test_classify_zero_spend_is_sipper():
21
21
 
22
22
  def test_classify_multiplier_path_walks_tier_boundaries():
23
23
  # Multiplier-based classification — the primary path for subscription users.
24
- # Boundaries are 1× / 4× / 10× / 20× per the launch-tier spec.
24
+ # Six tiers, boundaries at 1× / 4× / 10× / 20× / 50×.
25
25
  cases = [
26
26
  (0.99, "TokenSipper"),
27
27
  (1.0, "TokenModerator"),
28
28
  (3.99, "TokenModerator"),
29
29
  (4.0, "TokenMaxxer"),
30
30
  (9.99, "TokenMaxxer"),
31
- (10.0, "TokenChad"),
32
- (19.99, "TokenChad"),
33
- (20.0, "TokenGigaChad"),
34
- (200.0, "TokenGigaChad"),
31
+ (10.0, "TokenSuperMaxxer"),
32
+ (19.99, "TokenSuperMaxxer"),
33
+ (20.0, "TokenMegaMaxxer"),
34
+ (49.99, "TokenMegaMaxxer"),
35
+ (50.0, "TokenGigaMaxxer"),
36
+ (200.0, "TokenGigaMaxxer"),
35
37
  ]
36
38
  for mult, expected in cases:
37
39
  assert _classify(0.0, multiplier=mult).label == expected, f"failed at {mult}×"
@@ -46,10 +48,12 @@ def test_classify_absolute_path_for_api_users_walks_tier_boundaries():
46
48
  (399.99, "TokenModerator"),
47
49
  (400.0, "TokenMaxxer"),
48
50
  (999.99, "TokenMaxxer"),
49
- (1000.0, "TokenChad"),
50
- (1999.99, "TokenChad"),
51
- (2000.0, "TokenGigaChad"),
52
- (50_000, "TokenGigaChad"),
51
+ (1000.0, "TokenSuperMaxxer"),
52
+ (1999.99, "TokenSuperMaxxer"),
53
+ (2000.0, "TokenMegaMaxxer"),
54
+ (4999.99, "TokenMegaMaxxer"),
55
+ (5000.0, "TokenGigaMaxxer"),
56
+ (50_000, "TokenGigaMaxxer"),
53
57
  ]
54
58
  for spend, expected in cases:
55
59
  assert _classify(spend).label == expected, f"failed at ${spend}"
@@ -36,9 +36,23 @@ def test_calculate_cost_with_cache_write_tokens():
36
36
  cache_read_tokens=0,
37
37
  cache_write_tokens=1_000_000,
38
38
  )
39
- # Zero input/output but cache_write tokens present — still returns 0
40
- # because input_tokens == 0 and output_tokens == 0 triggers early return
41
- assert cost == 0.0
39
+ # Zero input/output but cache_write tokens present — the early return only
40
+ # fires when ALL token counts are zero, so cache_write cost is still charged.
41
+ # (1_000_000/1M * 1.00) = 1.00
42
+ assert cost == 1.0
43
+
44
+
45
+ def test_calculate_cost_cache_read_only():
46
+ # claude-haiku-4-5: cache_read=0.08 per MTok. A pure cache hit (no new
47
+ # input/output) still costs the cache-read rate and must not be dropped.
48
+ cost = calculate_cost(
49
+ "anthropic", "claude-haiku-4-5",
50
+ input_tokens=0,
51
+ output_tokens=0,
52
+ cache_read_tokens=1_000_000,
53
+ )
54
+ # (1_000_000/1M * 0.08) = 0.08
55
+ assert cost == 0.08
42
56
 
43
57
 
44
58
  def test_calculate_cost_unknown_model_uses_default(caplog):