tokenjam 0.2.1__tar.gz → 0.2.2__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 (204) hide show
  1. {tokenjam-0.2.1 → tokenjam-0.2.2}/PKG-INFO +1 -1
  2. {tokenjam-0.2.1 → tokenjam-0.2.2}/pyproject.toml +1 -1
  3. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/package-lock.json +2 -2
  4. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/package.json +1 -1
  5. tokenjam-0.2.2/tests/unit/test_litellm_client.py +214 -0
  6. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/__init__.py +2 -0
  7. tokenjam-0.2.2/tokenjam/sdk/client.py +267 -0
  8. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/CODEOWNERS +0 -0
  9. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
  12. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/pull_request_template.md +0 -0
  13. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/workflows/ci.yml +0 -0
  14. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/workflows/publish-npm.yml +0 -0
  15. {tokenjam-0.2.1 → tokenjam-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
  16. {tokenjam-0.2.1 → tokenjam-0.2.2}/.gitignore +0 -0
  17. {tokenjam-0.2.1 → tokenjam-0.2.2}/AGENTS.md +0 -0
  18. {tokenjam-0.2.1 → tokenjam-0.2.2}/CHANGELOG.md +0 -0
  19. {tokenjam-0.2.1 → tokenjam-0.2.2}/CLAUDE.md +0 -0
  20. {tokenjam-0.2.1 → tokenjam-0.2.2}/CONTRIBUTING.md +0 -0
  21. {tokenjam-0.2.1 → tokenjam-0.2.2}/LICENSE +0 -0
  22. {tokenjam-0.2.1 → tokenjam-0.2.2}/Makefile +0 -0
  23. {tokenjam-0.2.1 → tokenjam-0.2.2}/README.md +0 -0
  24. {tokenjam-0.2.1 → tokenjam-0.2.2}/SECURITY.md +0 -0
  25. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/alerts.md +0 -0
  26. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/architecture.md +0 -0
  27. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/claude-code-integration.md +0 -0
  28. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/cli-reference.md +0 -0
  29. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/configuration.md +0 -0
  30. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/export.md +0 -0
  31. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/framework-support.md +0 -0
  32. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/nemoclaw-integration.md +0 -0
  33. {tokenjam-0.2.1 → tokenjam-0.2.2}/docs/openclaw.md +0 -0
  34. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/README.md +0 -0
  35. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/alerts_and_drift/_shared.py +0 -0
  36. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
  37. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/alerts_and_drift/drift_demo.py +0 -0
  38. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
  39. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/rag_pipeline.py +0 -0
  40. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/research_team.py +0 -0
  41. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/router_agent.py +0 -0
  42. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/sample_docs/agent_patterns.txt +0 -0
  43. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/sample_docs/cost_management.txt +0 -0
  44. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/sample_docs/observability.txt +0 -0
  45. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/multi/sample_docs/safety.txt +0 -0
  46. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/openclaw/README.md +0 -0
  47. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_framework/autogen_agent.py +0 -0
  48. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_framework/crewai_agent.py +0 -0
  49. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_framework/langchain_agent.py +0 -0
  50. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_framework/langgraph_agent.py +0 -0
  51. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_framework/llamaindex_agent.py +0 -0
  52. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_provider/anthropic_agent.py +0 -0
  53. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_provider/bedrock_agent.py +0 -0
  54. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_provider/gemini_agent.py +0 -0
  55. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_provider/litellm_agent.py +0 -0
  56. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_provider/openai_agent.py +0 -0
  57. {tokenjam-0.2.1 → tokenjam-0.2.2}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
  58. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/hallucination-drift/BLOG.md +0 -0
  59. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/hallucination-drift/README.md +0 -0
  60. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/hallucination-drift/scenario.py +0 -0
  61. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/retry-loop/BLOG.md +0 -0
  62. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/retry-loop/README.md +0 -0
  63. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/retry-loop/scenario.py +0 -0
  64. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/surprise-cost/BLOG.md +0 -0
  65. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/surprise-cost/README.md +0 -0
  66. {tokenjam-0.2.1 → tokenjam-0.2.2}/incidents/surprise-cost/scenario.py +0 -0
  67. {tokenjam-0.2.1 → tokenjam-0.2.2}/pricing/models.toml +0 -0
  68. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/README.md +0 -0
  69. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/client.test.ts +0 -0
  70. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/client.ts +0 -0
  71. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/index.ts +0 -0
  72. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/semconv.test.ts +0 -0
  73. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/semconv.ts +0 -0
  74. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/span-builder.test.ts +0 -0
  75. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/span-builder.ts +0 -0
  76. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/src/types.ts +0 -0
  77. {tokenjam-0.2.1 → tokenjam-0.2.2}/sdk-ts/tsconfig.json +0 -0
  78. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/__init__.py +0 -0
  79. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/__init__.py +0 -0
  80. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/email_agent_budget_breach.py +0 -0
  81. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/email_agent_drift.py +0 -0
  82. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/email_agent_loop.py +0 -0
  83. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/email_agent_normal.py +0 -0
  84. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/mock_llm.py +0 -0
  85. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/agents/test_mock_scenarios.py +0 -0
  86. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/conftest.py +0 -0
  87. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/e2e/__init__.py +0 -0
  88. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/e2e/conftest.py +0 -0
  89. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/e2e/test_real_llm.py +0 -0
  90. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/factories.py +0 -0
  91. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/__init__.py +0 -0
  92. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/test_api.py +0 -0
  93. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/test_cli.py +0 -0
  94. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/test_db.py +0 -0
  95. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/test_demos.py +0 -0
  96. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/test_full_pipeline.py +0 -0
  97. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/integration/test_logs_api.py +0 -0
  98. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/manual-new-release-tests.md +0 -0
  99. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/manual-pre-release-testing.md +0 -0
  100. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/synthetic/__init__.py +0 -0
  101. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/synthetic/test_alert_rules.py +0 -0
  102. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/synthetic/test_cost_tracking.py +0 -0
  103. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/synthetic/test_drift_detection.py +0 -0
  104. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/synthetic/test_ingest.py +0 -0
  105. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/synthetic/test_schema_validation.py +0 -0
  106. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/toy_agent/toy_agent.py +0 -0
  107. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/__init__.py +0 -0
  108. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_alerts.py +0 -0
  109. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_cmd_stop.py +0 -0
  110. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_config.py +0 -0
  111. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_cost.py +0 -0
  112. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_demo_env.py +0 -0
  113. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_demo_scenarios.py +0 -0
  114. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_drift.py +0 -0
  115. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_formatting.py +0 -0
  116. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_litellm_integration.py +0 -0
  117. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_logs_converter.py +0 -0
  118. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_mcp_server.py +0 -0
  119. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_models.py +0 -0
  120. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_onboard_codex.py +0 -0
  121. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_onboard_daemon.py +0 -0
  122. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_openclaw_ingest.py +0 -0
  123. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_spans_stats_repair.py +0 -0
  124. {tokenjam-0.2.1 → tokenjam-0.2.2}/tests/unit/test_time_parse.py +0 -0
  125. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/__init__.py +0 -0
  126. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/__init__.py +0 -0
  127. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/app.py +0 -0
  128. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/deps.py +0 -0
  129. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/middleware.py +0 -0
  130. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/__init__.py +0 -0
  131. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/agents.py +0 -0
  132. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/alerts.py +0 -0
  133. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/budget.py +0 -0
  134. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/cost.py +0 -0
  135. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/drift.py +0 -0
  136. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/logs.py +0 -0
  137. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/metrics.py +0 -0
  138. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/otlp.py +0 -0
  139. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/spans.py +0 -0
  140. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/status.py +0 -0
  141. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/tools.py +0 -0
  142. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/api/routes/traces.py +0 -0
  143. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/__init__.py +0 -0
  144. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_alerts.py +0 -0
  145. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_budget.py +0 -0
  146. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_cost.py +0 -0
  147. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_demo.py +0 -0
  148. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_doctor.py +0 -0
  149. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_drift.py +0 -0
  150. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_export.py +0 -0
  151. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_mcp.py +0 -0
  152. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_onboard.py +0 -0
  153. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_serve.py +0 -0
  154. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_status.py +0 -0
  155. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_stop.py +0 -0
  156. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_tools.py +0 -0
  157. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_traces.py +0 -0
  158. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/cmd_uninstall.py +0 -0
  159. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/cli/main.py +0 -0
  160. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/__init__.py +0 -0
  161. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/alerts.py +0 -0
  162. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/api_backend.py +0 -0
  163. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/config.py +0 -0
  164. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/cost.py +0 -0
  165. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/db.py +0 -0
  166. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/drift.py +0 -0
  167. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/ingest.py +0 -0
  168. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/models.py +0 -0
  169. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/pricing.py +0 -0
  170. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/retention.py +0 -0
  171. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/core/schema_validator.py +0 -0
  172. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/demo/__init__.py +0 -0
  173. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/demo/env.py +0 -0
  174. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/mcp/__init__.py +0 -0
  175. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/mcp/server.py +0 -0
  176. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/otel/__init__.py +0 -0
  177. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/otel/exporters.py +0 -0
  178. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/otel/provider.py +0 -0
  179. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/otel/semconv.py +0 -0
  180. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/pricing/models.toml +0 -0
  181. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/py.typed +0 -0
  182. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/agent.py +0 -0
  183. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/bootstrap.py +0 -0
  184. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/http_exporter.py +0 -0
  185. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/__init__.py +0 -0
  186. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/anthropic.py +0 -0
  187. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/autogen.py +0 -0
  188. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/base.py +0 -0
  189. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/bedrock.py +0 -0
  190. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/crewai.py +0 -0
  191. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/gemini.py +0 -0
  192. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/langchain.py +0 -0
  193. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/langgraph.py +0 -0
  194. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/litellm.py +0 -0
  195. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/llamaindex.py +0 -0
  196. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
  197. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/openai.py +0 -0
  198. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
  199. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/sdk/transport.py +0 -0
  200. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/ui/index.html +0 -0
  201. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/utils/__init__.py +0 -0
  202. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/utils/formatting.py +0 -0
  203. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/utils/ids.py +0 -0
  204. {tokenjam-0.2.1 → tokenjam-0.2.2}/tokenjam/utils/time_parse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenjam
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tokenjam"
7
- version = "0.2.1"
7
+ version = "0.2.2"
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,12 +1,12 @@
1
1
  {
2
2
  "name": "@tokenjam/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@tokenjam/sdk",
9
- "version": "0.2.1",
9
+ "version": "0.2.2",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
12
  "@types/node": "^25.5.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenjam/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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,214 @@
1
+ """Unit tests for TokenJamClient.emit_litellm_span (LiteLLM named-callback path)."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ from types import SimpleNamespace
6
+ from unittest.mock import patch
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from tokenjam.otel.semconv import GenAIAttributes, TjAttributes
12
+ from tokenjam.sdk.client import TokenJamClient, _build_litellm_span
13
+
14
+
15
+ def _attrs_to_dict(otlp_attrs: list[dict]) -> dict[str, object]:
16
+ out: dict[str, object] = {}
17
+ for a in otlp_attrs:
18
+ v = a["value"]
19
+ if "stringValue" in v:
20
+ out[a["key"]] = v["stringValue"]
21
+ elif "intValue" in v:
22
+ out[a["key"]] = int(v["intValue"])
23
+ elif "doubleValue" in v:
24
+ out[a["key"]] = float(v["doubleValue"])
25
+ elif "boolValue" in v:
26
+ out[a["key"]] = v["boolValue"]
27
+ return out
28
+
29
+
30
+ def _basic_response(provider: str = "openai", prompt: int = 10, completion: int = 5) -> object:
31
+ return SimpleNamespace(
32
+ usage=SimpleNamespace(prompt_tokens=prompt, completion_tokens=completion),
33
+ _hidden_params={"custom_llm_provider": provider, "response_cost": 0.00042},
34
+ )
35
+
36
+
37
+ # --- _build_litellm_span ---------------------------------------------------
38
+
39
+
40
+ def test_build_span_success_with_usage_and_cost():
41
+ kwargs = {
42
+ "model": "openai/gpt-4o-mini",
43
+ "metadata": {"tj_agent_id": "agent-a", "tj_session_id": "sess-1"},
44
+ }
45
+ response = _basic_response()
46
+ start = datetime(2026, 5, 12, tzinfo=timezone.utc)
47
+ end = datetime(2026, 5, 12, 0, 0, 1, tzinfo=timezone.utc)
48
+
49
+ span = _build_litellm_span(kwargs, response, start, end, success=True)
50
+
51
+ assert span["name"] == GenAIAttributes.SPAN_LLM_CALL
52
+ assert span["status"] == {"code": 1}
53
+ assert len(span["traceId"]) == 32
54
+ assert len(span["spanId"]) == 16
55
+
56
+ attrs = _attrs_to_dict(span["attributes"])
57
+ assert attrs[GenAIAttributes.REQUEST_MODEL] == "gpt-4o-mini" # prefix stripped
58
+ assert attrs[GenAIAttributes.PROVIDER_NAME] == "openai"
59
+ assert attrs[GenAIAttributes.INPUT_TOKENS] == 10
60
+ assert attrs[GenAIAttributes.OUTPUT_TOKENS] == 5
61
+ assert attrs[TjAttributes.COST_USD] == pytest.approx(0.00042)
62
+ assert attrs[GenAIAttributes.AGENT_ID] == "agent-a"
63
+ assert attrs[GenAIAttributes.CONVERSATION_ID] == "sess-1"
64
+
65
+
66
+ def test_build_span_failure_records_error_status():
67
+ kwargs = {"model": "anthropic/claude-3-5-sonnet"}
68
+ err = RuntimeError("rate limited")
69
+ start = datetime.now(timezone.utc)
70
+ end = datetime.now(timezone.utc)
71
+
72
+ span = _build_litellm_span(kwargs, err, start, end, success=False)
73
+ assert span["status"]["code"] == 2
74
+ assert span["status"]["message"] == "rate limited"
75
+
76
+
77
+ def test_build_span_falls_back_to_model_prefix_for_provider():
78
+ # No _hidden_params on response — provider inferred from model prefix.
79
+ kwargs = {"model": "anthropic/claude-3-5-sonnet"}
80
+ response = SimpleNamespace(usage=None)
81
+ span = _build_litellm_span(
82
+ kwargs, response,
83
+ datetime.now(timezone.utc), datetime.now(timezone.utc),
84
+ success=True,
85
+ )
86
+ attrs = _attrs_to_dict(span["attributes"])
87
+ assert attrs[GenAIAttributes.PROVIDER_NAME] == "anthropic"
88
+ assert attrs[GenAIAttributes.REQUEST_MODEL] == "claude-3-5-sonnet"
89
+
90
+
91
+ def test_build_span_handles_dict_usage():
92
+ kwargs = {"model": "gpt-4o"}
93
+ response = {
94
+ "usage": {
95
+ "prompt_tokens": 7,
96
+ "completion_tokens": 3,
97
+ "cache_read_input_tokens": 100,
98
+ "cache_creation_input_tokens": 50,
99
+ },
100
+ "_hidden_params": {"custom_llm_provider": "openai"},
101
+ }
102
+ span = _build_litellm_span(
103
+ kwargs, response,
104
+ datetime.now(timezone.utc), datetime.now(timezone.utc),
105
+ success=True,
106
+ )
107
+ attrs = _attrs_to_dict(span["attributes"])
108
+ assert attrs[GenAIAttributes.INPUT_TOKENS] == 7
109
+ assert attrs[GenAIAttributes.OUTPUT_TOKENS] == 3
110
+ assert attrs[GenAIAttributes.CACHE_READ_TOKENS] == 100
111
+ assert attrs[GenAIAttributes.CACHE_CREATE_TOKENS] == 50
112
+
113
+
114
+ def test_build_span_missing_model_defaults_to_unknown():
115
+ span = _build_litellm_span(
116
+ {}, SimpleNamespace(usage=None),
117
+ datetime.now(timezone.utc), datetime.now(timezone.utc),
118
+ success=True,
119
+ )
120
+ attrs = _attrs_to_dict(span["attributes"])
121
+ assert attrs[GenAIAttributes.REQUEST_MODEL] == "unknown"
122
+ assert attrs[GenAIAttributes.PROVIDER_NAME] == "litellm"
123
+
124
+
125
+ # --- TokenJamClient HTTP behavior ------------------------------------------
126
+
127
+
128
+ def test_client_appends_api_v1_spans_to_base_endpoint():
129
+ c = TokenJamClient(endpoint="http://localhost:7391")
130
+ assert c._endpoint == "http://localhost:7391/api/v1/spans"
131
+
132
+ c2 = TokenJamClient(endpoint="http://localhost:7391/api/v1/spans")
133
+ assert c2._endpoint == "http://localhost:7391/api/v1/spans"
134
+
135
+ c3 = TokenJamClient(endpoint="http://localhost:7391/")
136
+ assert c3._endpoint == "http://localhost:7391/api/v1/spans"
137
+
138
+
139
+ def test_client_posts_otlp_payload_with_bearer_when_secret_set():
140
+ c = TokenJamClient(endpoint="http://localhost:7391", ingest_secret="s3cret")
141
+
142
+ captured: dict[str, object] = {}
143
+
144
+ def fake_post(url, json, headers, timeout):
145
+ captured["url"] = url
146
+ captured["json"] = json
147
+ captured["headers"] = headers
148
+ return httpx.Response(200)
149
+
150
+ with patch("tokenjam.sdk.client.httpx.post", side_effect=fake_post):
151
+ c.emit_litellm_span(
152
+ kwargs={"model": "openai/gpt-4o-mini"},
153
+ response_obj=_basic_response(),
154
+ start_time=datetime.now(timezone.utc),
155
+ end_time=datetime.now(timezone.utc),
156
+ success=True,
157
+ )
158
+
159
+ assert captured["url"] == "http://localhost:7391/api/v1/spans"
160
+ assert captured["headers"]["Authorization"] == "Bearer s3cret"
161
+ body = captured["json"]
162
+ assert "resourceSpans" in body
163
+ assert body["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["name"] == GenAIAttributes.SPAN_LLM_CALL
164
+
165
+
166
+ def test_client_omits_authorization_when_no_secret():
167
+ c = TokenJamClient(endpoint="http://localhost:7391")
168
+ captured: dict[str, object] = {}
169
+
170
+ def fake_post(url, json, headers, timeout):
171
+ captured["headers"] = headers
172
+ return httpx.Response(200)
173
+
174
+ with patch("tokenjam.sdk.client.httpx.post", side_effect=fake_post):
175
+ c.emit_litellm_span(
176
+ kwargs={"model": "gpt-4o-mini"},
177
+ response_obj=_basic_response(),
178
+ start_time=datetime.now(timezone.utc),
179
+ end_time=datetime.now(timezone.utc),
180
+ success=True,
181
+ )
182
+
183
+ assert "Authorization" not in captured["headers"]
184
+
185
+
186
+ def test_client_swallows_connection_errors():
187
+ c = TokenJamClient(endpoint="http://localhost:7391")
188
+ with patch(
189
+ "tokenjam.sdk.client.httpx.post",
190
+ side_effect=httpx.ConnectError("boom"),
191
+ ):
192
+ # Must not raise.
193
+ c.emit_litellm_span(
194
+ kwargs={"model": "gpt-4o-mini"},
195
+ response_obj=_basic_response(),
196
+ start_time=datetime.now(timezone.utc),
197
+ end_time=datetime.now(timezone.utc),
198
+ success=True,
199
+ )
200
+
201
+
202
+ def test_client_swallows_build_errors():
203
+ """A malformed response should not propagate; the event is dropped."""
204
+ c = TokenJamClient(endpoint="http://localhost:7391")
205
+ with patch(
206
+ "tokenjam.sdk.client._build_litellm_span",
207
+ side_effect=ValueError("bad payload"),
208
+ ):
209
+ c.emit_litellm_span(
210
+ kwargs={}, response_obj=None,
211
+ start_time=datetime.now(timezone.utc),
212
+ end_time=datetime.now(timezone.utc),
213
+ success=True,
214
+ )
@@ -1,4 +1,5 @@
1
1
  from tokenjam.sdk.agent import watch, AgentSession, record_llm_call, record_tool_call
2
+ from tokenjam.sdk.client import TokenJamClient
2
3
  from tokenjam.sdk.integrations.anthropic import patch_anthropic
3
4
  from tokenjam.sdk.integrations.openai import patch_openai
4
5
  from tokenjam.sdk.integrations.gemini import patch_gemini
@@ -14,6 +15,7 @@ from tokenjam.sdk.integrations.nemoclaw import watch_nemoclaw
14
15
 
15
16
  __all__ = [
16
17
  "watch", "AgentSession", "record_llm_call", "record_tool_call",
18
+ "TokenJamClient",
17
19
  "patch_anthropic", "patch_openai", "patch_gemini", "patch_bedrock",
18
20
  "patch_langchain", "patch_langgraph", "patch_crewai", "patch_autogen",
19
21
  "patch_litellm", "patch_llamaindex", "patch_openai_agents",
@@ -0,0 +1,267 @@
1
+ """Public HTTP client for shipping spans into a running ``tj serve``.
2
+
3
+ This is the entry point used by external integrations that cannot rely on the
4
+ in-process OTel TracerProvider — most notably, the upstream LiteLLM named
5
+ callback (``litellm.success_callback = ["tokenjam"]``) which lives in the
6
+ LiteLLM repo and only depends on this public surface.
7
+
8
+ For in-process use inside a tokenjam-aware app, prefer ``patch_litellm()`` —
9
+ it produces the same spans via the OTel pipeline.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import secrets
15
+ from datetime import datetime, timezone
16
+ from typing import Any, Optional
17
+
18
+ import httpx
19
+
20
+ from tokenjam.otel.semconv import GenAIAttributes, TjAttributes
21
+
22
+ logger = logging.getLogger("tokenjam.sdk")
23
+
24
+ _TIMEOUT_SECS = 5.0
25
+
26
+
27
+ class TokenJamClient:
28
+ """Thin HTTP client that POSTs a single LiteLLM call as an OTLP span.
29
+
30
+ Designed to be embedded in foreign codebases (e.g. LiteLLM's named-callback
31
+ machinery), so it has no dependency on the rest of the tokenjam SDK at
32
+ construction time and never raises from its public methods — failures are
33
+ logged at ``debug`` and the event is dropped.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ endpoint: str = "http://localhost:7391",
39
+ ingest_secret: Optional[str] = None,
40
+ ) -> None:
41
+ # Accept either the server base URL or the full /api/v1/spans path.
42
+ endpoint = endpoint.rstrip("/")
43
+ if not endpoint.endswith("/api/v1/spans"):
44
+ endpoint = f"{endpoint}/api/v1/spans"
45
+ self._endpoint = endpoint
46
+ self._secret = ingest_secret
47
+
48
+ def emit_litellm_span(
49
+ self,
50
+ kwargs: dict[str, Any],
51
+ response_obj: Any,
52
+ start_time: datetime,
53
+ end_time: datetime,
54
+ success: bool,
55
+ ) -> None:
56
+ """Translate a LiteLLM success/failure callback payload into an OTLP
57
+ span and POST it to ``tj serve``.
58
+
59
+ Non-blocking: all errors are swallowed and logged at debug.
60
+ """
61
+ try:
62
+ span = _build_litellm_span(
63
+ kwargs=kwargs,
64
+ response_obj=response_obj,
65
+ start_time=start_time,
66
+ end_time=end_time,
67
+ success=success,
68
+ )
69
+ payload = {
70
+ "resourceSpans": [{
71
+ "resource": {"attributes": [
72
+ {"key": "service.name",
73
+ "value": {"stringValue": "litellm"}},
74
+ ]},
75
+ "scopeSpans": [{"spans": [span]}],
76
+ }],
77
+ }
78
+ headers = {"Content-Type": "application/json"}
79
+ if self._secret:
80
+ headers["Authorization"] = f"Bearer {self._secret}"
81
+ resp = httpx.post(
82
+ self._endpoint,
83
+ json=payload,
84
+ headers=headers,
85
+ timeout=_TIMEOUT_SECS,
86
+ )
87
+ if resp.status_code >= 300:
88
+ logger.debug(
89
+ "tj serve returned %d on emit_litellm_span",
90
+ resp.status_code,
91
+ )
92
+ except Exception as exc: # noqa: BLE001 — non-blocking by design
93
+ logger.debug("emit_litellm_span failed: %s", exc)
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Payload construction (pure functions, exposed for unit testing)
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def _build_litellm_span(
102
+ kwargs: dict[str, Any],
103
+ response_obj: Any,
104
+ start_time: datetime,
105
+ end_time: datetime,
106
+ success: bool,
107
+ ) -> dict[str, Any]:
108
+ """Build an OTLP JSON span dict from a LiteLLM callback payload."""
109
+ raw_model = str(kwargs.get("model") or "unknown")
110
+ model = _strip_provider_prefix(raw_model)
111
+ provider = _parse_provider(raw_model, response_obj)
112
+ metadata = kwargs.get("metadata") or {}
113
+
114
+ attrs: dict[str, Any] = {
115
+ GenAIAttributes.REQUEST_MODEL: model,
116
+ GenAIAttributes.PROVIDER_NAME: provider,
117
+ }
118
+
119
+ # Token usage from response.usage (OpenAI-style dict or pydantic model)
120
+ usage = _get_usage(response_obj)
121
+ if usage:
122
+ prompt_tokens = _coerce_int(usage.get("prompt_tokens"))
123
+ completion_tokens = _coerce_int(usage.get("completion_tokens"))
124
+ if prompt_tokens is not None:
125
+ attrs[GenAIAttributes.INPUT_TOKENS] = prompt_tokens
126
+ if completion_tokens is not None:
127
+ attrs[GenAIAttributes.OUTPUT_TOKENS] = completion_tokens
128
+ # Anthropic-style cache token fields, if present
129
+ cache_read = _coerce_int(usage.get("cache_read_input_tokens"))
130
+ cache_create = _coerce_int(usage.get("cache_creation_input_tokens"))
131
+ if cache_read is not None:
132
+ attrs[GenAIAttributes.CACHE_READ_TOKENS] = cache_read
133
+ if cache_create is not None:
134
+ attrs[GenAIAttributes.CACHE_CREATE_TOKENS] = cache_create
135
+
136
+ # LiteLLM puts a precomputed cost on kwargs / response.hidden_params
137
+ cost = _get_response_cost(kwargs, response_obj)
138
+ if cost is not None:
139
+ attrs[TjAttributes.COST_USD] = cost
140
+
141
+ # Per-call agent + session tags supplied via metadata
142
+ if isinstance(metadata, dict):
143
+ agent_id = metadata.get("tj_agent_id")
144
+ if agent_id:
145
+ attrs[GenAIAttributes.AGENT_ID] = str(agent_id)
146
+ session_id = metadata.get("tj_session_id")
147
+ if session_id:
148
+ attrs[GenAIAttributes.CONVERSATION_ID] = str(session_id)
149
+
150
+ span: dict[str, Any] = {
151
+ "traceId": _new_trace_id(),
152
+ "spanId": _new_span_id(),
153
+ "name": GenAIAttributes.SPAN_LLM_CALL,
154
+ "kind": 3, # CLIENT
155
+ "startTimeUnixNano": str(_to_unix_nanos(start_time)),
156
+ "endTimeUnixNano": str(_to_unix_nanos(end_time)),
157
+ "attributes": [
158
+ {"key": k, "value": _to_otlp_value(v)} for k, v in attrs.items()
159
+ ],
160
+ "status": {"code": 1 if success else 2},
161
+ "events": [],
162
+ }
163
+ if not success:
164
+ msg = _extract_error_message(response_obj)
165
+ if msg:
166
+ span["status"]["message"] = msg
167
+ return span
168
+
169
+
170
+ def _strip_provider_prefix(model: str) -> str:
171
+ if "/" in model:
172
+ return model.split("/", 1)[1]
173
+ return model
174
+
175
+
176
+ def _parse_provider(model: str, response: Any) -> str:
177
+ hidden = getattr(response, "_hidden_params", None)
178
+ if isinstance(hidden, dict):
179
+ p = hidden.get("custom_llm_provider")
180
+ if p:
181
+ return str(p)
182
+ if isinstance(response, dict):
183
+ hidden = response.get("_hidden_params")
184
+ if isinstance(hidden, dict) and hidden.get("custom_llm_provider"):
185
+ return str(hidden["custom_llm_provider"])
186
+ if "/" in model:
187
+ return model.split("/", 1)[0]
188
+ return "litellm"
189
+
190
+
191
+ def _get_usage(response: Any) -> Optional[dict[str, Any]]:
192
+ """Normalize response.usage to a dict, regardless of pydantic/dict shape."""
193
+ usage = getattr(response, "usage", None)
194
+ if usage is None and isinstance(response, dict):
195
+ usage = response.get("usage")
196
+ if usage is None:
197
+ return None
198
+ if isinstance(usage, dict):
199
+ return usage
200
+ # pydantic v2 model or plain object with attributes
201
+ out: dict[str, Any] = {}
202
+ for k in (
203
+ "prompt_tokens", "completion_tokens",
204
+ "cache_read_input_tokens", "cache_creation_input_tokens",
205
+ ):
206
+ v = getattr(usage, k, None)
207
+ if v is not None:
208
+ out[k] = v
209
+ return out or None
210
+
211
+
212
+ def _get_response_cost(kwargs: dict[str, Any], response: Any) -> Optional[float]:
213
+ # LiteLLM exposes response_cost on the callback kwargs
214
+ cost = kwargs.get("response_cost")
215
+ if cost is None:
216
+ hidden = getattr(response, "_hidden_params", None)
217
+ if isinstance(hidden, dict):
218
+ cost = hidden.get("response_cost")
219
+ if cost is None:
220
+ return None
221
+ try:
222
+ return float(cost)
223
+ except (TypeError, ValueError):
224
+ return None
225
+
226
+
227
+ def _extract_error_message(response: Any) -> Optional[str]:
228
+ if isinstance(response, Exception):
229
+ return str(response)
230
+ if isinstance(response, dict):
231
+ err = response.get("error") or response.get("message")
232
+ if err:
233
+ return str(err)
234
+ return None
235
+
236
+
237
+ def _coerce_int(v: Any) -> Optional[int]:
238
+ if v is None:
239
+ return None
240
+ try:
241
+ return int(v)
242
+ except (TypeError, ValueError):
243
+ return None
244
+
245
+
246
+ def _to_unix_nanos(dt: datetime) -> int:
247
+ if dt.tzinfo is None:
248
+ dt = dt.replace(tzinfo=timezone.utc)
249
+ return int(dt.timestamp() * 1_000_000_000)
250
+
251
+
252
+ def _new_trace_id() -> str:
253
+ return secrets.token_hex(16)
254
+
255
+
256
+ def _new_span_id() -> str:
257
+ return secrets.token_hex(8)
258
+
259
+
260
+ def _to_otlp_value(v: Any) -> dict[str, Any]:
261
+ if isinstance(v, bool):
262
+ return {"boolValue": v}
263
+ if isinstance(v, int):
264
+ return {"intValue": str(v)}
265
+ if isinstance(v, float):
266
+ return {"doubleValue": v}
267
+ return {"stringValue": str(v)}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes