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.
- {tokenjam-0.4.2 → tokenjam-0.5.1}/CLAUDE.md +2 -1
- {tokenjam-0.4.2 → tokenjam-0.5.1}/CONTRIBUTING.md +17 -7
- {tokenjam-0.4.2 → tokenjam-0.5.1}/PKG-INFO +26 -2
- {tokenjam-0.4.2 → tokenjam-0.5.1}/README.md +25 -1
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/claude-code-integration.md +2 -2
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/installation.md +3 -3
- tokenjam-0.5.1/docs/internal/release-smoke-checklist.md +126 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/trim.md +1 -1
- {tokenjam-0.4.2 → tokenjam-0.5.1}/pyproject.toml +1 -1
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/package.json +1 -1
- tokenjam-0.5.1/tests/integration/test_analytics.py +191 -0
- tokenjam-0.5.1/tests/integration/test_cmd_route.py +163 -0
- tokenjam-0.5.1/tests/integration/test_cost_charts.py +198 -0
- tokenjam-0.5.1/tests/integration/test_first_run_roundtrip_239.py +303 -0
- tokenjam-0.5.1/tests/unit/test_backfill.py +437 -0
- tokenjam-0.5.1/tests/unit/test_export_route_generators.py +175 -0
- tokenjam-0.5.1/tests/unit/test_lens_ui_regression.py +904 -0
- tokenjam-0.5.1/tests/unit/test_onboard_first_run.py +155 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_workflow_restructure.py +25 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/app.py +2 -0
- tokenjam-0.5.1/tokenjam/api/routes/analytics.py +289 -0
- tokenjam-0.5.1/tokenjam/api/routes/cost.py +468 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/traces.py +4 -0
- tokenjam-0.5.1/tokenjam/cli/banner.py +36 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_backfill.py +14 -3
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_onboard.py +89 -32
- tokenjam-0.5.1/tokenjam/cli/cmd_route.py +239 -0
- tokenjam-0.5.1/tokenjam/cli/home.py +46 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/main.py +14 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/backfill.py +66 -10
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/db.py +4 -1
- tokenjam-0.5.1/tokenjam/core/export/ccr.py +106 -0
- tokenjam-0.5.1/tokenjam/core/export/common.py +101 -0
- tokenjam-0.5.1/tokenjam/core/export/litellm.py +102 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/models.py +6 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/workflow_restructure.py +13 -1
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/index.html +1367 -236
- tokenjam-0.4.2/tests/unit/test_backfill.py +0 -231
- tokenjam-0.4.2/tests/unit/test_lens_ui_regression.py +0 -291
- tokenjam-0.4.2/tokenjam/api/routes/cost.py +0 -157
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/CODEOWNERS +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/pull_request_template.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/scripts/archive_traffic.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/ci.yml +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/publish-npm.yml +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/publish-pypi.yml +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.github/workflows/traffic-archive.yml +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/.gitignore +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/AGENTS.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/LICENSE +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/Makefile +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/SECURITY.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/alerts.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/architecture.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/helicone.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/langfuse.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/otlp.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/backfill/overview.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/cli-reference.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/configuration.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/export.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/framework-support.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/internal/lens-vendor-versions.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/internal/specs/.gitkeep +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/nemoclaw-integration.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/openclaw.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/cache.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/downsize.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/reuse.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/optimize/script.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/policy/overview.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/python-sdk.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-alerts.png +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-budget.png +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-cost.png +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-status.png +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/screenshots/tj-traces.png +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/docs/typescript-sdk.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/_shared.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/drift_demo.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/rag_pipeline.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/research_team.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/router_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/agent_patterns.txt +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/cost_management.txt +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/observability.txt +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/multi/sample_docs/safety.txt +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/openclaw/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/autogen_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/crewai_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/langchain_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/langgraph_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_framework/llamaindex_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/anthropic_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/bedrock_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/gemini_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/litellm_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/openai_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/growth/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/hallucination-drift/BLOG.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/hallucination-drift/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/hallucination-drift/scenario.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/retry-loop/BLOG.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/retry-loop/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/retry-loop/scenario.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/surprise-cost/BLOG.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/surprise-cost/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/incidents/surprise-cost/scenario.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/package-lock.json +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/client.test.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/client.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/index.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/semconv.test.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/semconv.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/span-builder.test.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/span-builder.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/src/types.ts +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/sdk-ts/tsconfig.json +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-post-release-runner.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-post-release-v0.4.0.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-pre-release-runner.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-pre-release-v0.4.0.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agent-pre-release-v0.4.1.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_budget_breach.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_drift.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_loop.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/email_agent_normal.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/mock_llm.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/agents/test_mock_scenarios.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/conftest.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/e2e/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/e2e/conftest.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/e2e/test_real_llm.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/factories.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/fixtures/helicone_real_response.json +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/fixtures/langfuse_real_response.json +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/fixtures/otlp_sample.json +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_api.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_cli.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_cli_api_framing_parity.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_cost_framing_window.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_db.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_demos.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_full_pipeline.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_litellm_enrichment.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_logs_api.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/integration/test_sdk_config_discovery.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/manual-new-release-tests.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/manual-pre-release-testing.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-post-release-v0.4.0-20260619T221412Z.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-pre-release-v0.4.0-20260619T202233Z.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-pre-release-v0.4.0-20260619T205108Z.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_alert_rules.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_cost_tracking.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_drift_detection.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_ingest.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/synthetic/test_schema_validation.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/toy_agent/toy_agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_alerts.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cache_efficacy.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cache_recommend.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_cost.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_policy.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_stop.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cmd_tokenmaxx.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_compare.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_config.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_config_secret_divergence.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cost.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cost_compare_framing.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_cycle.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_db_helpers.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_demo_env.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_demo_scenarios.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_drift.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_export_claude_code.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_formatting.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_framing.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ingest_helicone.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ingest_langfuse.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ingest_otlp.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_litellm_client.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_litellm_integration.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_logs_converter.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_mcp_server.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_models.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_no_tracked_dev_secrets.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_codex.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_daemon.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_hygiene.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_onboard_plan.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_openclaw_ingest.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_optimize.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_optimize_recoverable.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_optimize_reuse.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_plan_tier_apply.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_pricing_override.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_prompt_bloat.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_provider_attribution_194.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_reuse_skeleton.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_spans_stats_repair.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_time_parse.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_transport_401.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tests/unit/test_ui_offline.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/deps.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/middleware.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/agents.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/alerts.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/budget.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/cost_compare.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/drift.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/logs.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/metrics.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/optimize.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/otlp.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/reuse.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/spans.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/status.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/tools.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/api/routes/version.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_alerts.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_budget.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_cost.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_demo.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_doctor.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_drift.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_export.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_mcp.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_optimize.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_policy.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_report.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_serve.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_status.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_stop.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_tokenmaxx.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_tools.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_traces.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/cli/cmd_uninstall.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/alerts.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/api_backend.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/config.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/cost.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/cycle.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/drift.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/claude_code.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/reuse_report.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/export/reuse_skeleton.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/framing.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/helicone.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/ingest_adapters/otlp.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/README.md +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/model_downgrade.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/plan_reuse.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/registry.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/runner.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/optimize/types.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/pricing.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/retention.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/core/schema_validator.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/demo/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/demo/env.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/mcp/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/mcp/server.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/exporters.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/otlp_parsing.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/provider.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/otel/semconv.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/pricing/models.toml +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/py.typed +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/agent.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/bootstrap.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/client.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/http_exporter.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/anthropic.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/autogen.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/base.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/bedrock.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/crewai.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/gemini.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/langchain.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/langgraph.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/litellm.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/llamaindex.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/openai.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/sdk/transport.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/htm.js +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/preact-hooks.js +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/preact.js +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/uplot.css +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/ui/vendor/uplot.js +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/__init__.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/formatting.py +0 -0
- {tokenjam-0.4.2 → tokenjam-0.5.1}/tokenjam/utils/ids.py +0 -0
- {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]`
|
|
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.
|
|
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
|
|
11
|
-
pip install anthropic
|
|
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
|
|
61
|
+
## Using coding agents
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
TokenJam is built by AI coding agents, and contributing with one is first-class:
|
|
54
64
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
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.
|
|
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
|
[](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml)
|
|
70
70
|
[](https://pypi.org/project/tokenjam/)
|
|
71
|
+
[](https://pypi.org/project/tokenjam/)
|
|
71
72
|
[](https://pypi.org/project/tokenjam/)
|
|
72
73
|
[](https://www.npmjs.com/package/@tokenjam/sdk)
|
|
73
74
|
[](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
|
-
**
|
|
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
|
[](https://github.com/Metabuilder-Labs/tokenjam/actions/workflows/ci.yml)
|
|
12
12
|
[](https://pypi.org/project/tokenjam/)
|
|
13
|
+
[](https://pypi.org/project/tokenjam/)
|
|
13
14
|
[](https://pypi.org/project/tokenjam/)
|
|
14
15
|
[](https://www.npmjs.com/package/@tokenjam/sdk)
|
|
15
16
|
[](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
|
-
**
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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[
|
|
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 `
|
|
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
|
|
|
@@ -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)
|