tokenjam 0.3.4__tar.gz → 0.4.0__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.3.4 → tokenjam-0.4.0}/.gitignore +4 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/CLAUDE.md +64 -17
- {tokenjam-0.3.4 → tokenjam-0.4.0}/PKG-INFO +6 -2
- {tokenjam-0.3.4 → tokenjam-0.4.0}/README.md +4 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/installation.md +15 -0
- tokenjam-0.4.0/docs/internal/lens-vendor-versions.md +22 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/pyproject.toml +10 -2
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/package.json +1 -1
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_api.py +148 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_cli.py +100 -0
- tokenjam-0.4.0/tests/manual-new-release-tests.md +271 -0
- tokenjam-0.4.0/tests/manual-pre-release-runner.md +119 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/manual-pre-release-testing.md +63 -0
- tokenjam-0.4.0/tests/manual-pre-release-v0.4.0.md +291 -0
- tokenjam-0.4.0/tests/results/manual-pre-release-v0.4.0-20260619T202233Z.md +301 -0
- tokenjam-0.4.0/tests/results/manual-pre-release-v0.4.0-20260619T205108Z.md +293 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cmd_tokenmaxx.py +21 -2
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cost.py +52 -2
- tokenjam-0.4.0/tests/unit/test_cost_compare_framing.py +87 -0
- tokenjam-0.4.0/tests/unit/test_framing.py +249 -0
- tokenjam-0.4.0/tests/unit/test_lens_ui_regression.py +123 -0
- tokenjam-0.4.0/tests/unit/test_no_tracked_dev_secrets.py +31 -0
- tokenjam-0.4.0/tests/unit/test_onboard_hygiene.py +39 -0
- tokenjam-0.4.0/tests/unit/test_onboard_plan.py +65 -0
- tokenjam-0.4.0/tests/unit/test_optimize_recoverable.py +209 -0
- tokenjam-0.4.0/tests/unit/test_optimize_reuse.py +248 -0
- tokenjam-0.4.0/tests/unit/test_reuse_skeleton.py +106 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ui_offline.py +11 -1
- tokenjam-0.4.0/tokenjam/__init__.py +6 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/app.py +4 -1
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/budget.py +7 -0
- tokenjam-0.4.0/tokenjam/api/routes/cost.py +125 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/cost_compare.py +18 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/optimize.py +38 -14
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/traces.py +2 -1
- tokenjam-0.4.0/tokenjam/api/routes/version.py +20 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_cost.py +118 -53
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_onboard.py +23 -3
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_optimize.py +152 -89
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_report.py +97 -7
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_tokenmaxx.py +3 -30
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_uninstall.py +15 -1
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/main.py +5 -1
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/api_backend.py +5 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/cost.py +15 -5
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/db.py +17 -11
- tokenjam-0.4.0/tokenjam/core/export/reuse_report.py +418 -0
- tokenjam-0.4.0/tokenjam/core/export/reuse_skeleton.py +75 -0
- tokenjam-0.4.0/tokenjam/core/framing.py +333 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/models.py +2 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/__init__.py +6 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/cache_efficacy.py +60 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/model_downgrade.py +11 -0
- tokenjam-0.4.0/tokenjam/core/optimize/analyzers/plan_reuse.py +302 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/prompt_bloat.py +69 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/workflow_restructure.py +27 -2
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/runner.py +46 -6
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/types.py +66 -1
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/mcp/server.py +14 -1
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/pricing/models.toml +36 -0
- tokenjam-0.4.0/tokenjam/ui/index.html +2284 -0
- tokenjam-0.4.0/tokenjam/ui/vendor/uplot.css +2 -0
- tokenjam-0.4.0/tokenjam/ui/vendor/uplot.js +6 -0
- tokenjam-0.3.4/tests/manual-new-release-tests.md +0 -180
- tokenjam-0.3.4/tokenjam/__init__.py +0 -1
- tokenjam-0.3.4/tokenjam/api/routes/cost.py +0 -43
- tokenjam-0.3.4/tokenjam/ui/index.html +0 -1313
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/CODEOWNERS +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/pull_request_template.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/workflows/ci.yml +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/workflows/publish-npm.yml +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/.github/workflows/publish-pypi.yml +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/AGENTS.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/CHANGELOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/CONTRIBUTING.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/LICENSE +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/Makefile +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/SECURITY.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/alerts.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/architecture.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/helicone.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/langfuse.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/otlp.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/backfill/overview.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/claude-code-integration.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/cli-reference.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/configuration.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/export.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/framework-support.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/internal/specs/.gitkeep +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/nemoclaw-integration.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/openclaw.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/cache.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/downsize.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/script.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/optimize/trim.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/policy/overview.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/python-sdk.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-alerts.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-budget.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-cost.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-status.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/screenshots/tj-traces.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/docs/typescript-sdk.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/_shared.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/drift_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/rag_pipeline.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/research_team.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/router_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/agent_patterns.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/cost_management.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/observability.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/multi/sample_docs/safety.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/openclaw/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/autogen_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/crewai_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/langchain_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/langgraph_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_framework/llamaindex_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/anthropic_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/bedrock_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/gemini_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/litellm_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/openai_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/hallucination-drift/BLOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/hallucination-drift/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/hallucination-drift/scenario.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/retry-loop/BLOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/retry-loop/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/retry-loop/scenario.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/surprise-cost/BLOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/surprise-cost/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/incidents/surprise-cost/scenario.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/package-lock.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/client.test.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/client.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/index.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/semconv.test.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/semconv.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/span-builder.test.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/span-builder.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/src/types.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/sdk-ts/tsconfig.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_budget_breach.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_loop.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/email_agent_normal.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/mock_llm.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/agents/test_mock_scenarios.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/conftest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/e2e/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/e2e/conftest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/e2e/test_real_llm.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/factories.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/fixtures/helicone_real_response.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/fixtures/langfuse_real_response.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/fixtures/otlp_sample.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_db.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_demos.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_full_pipeline.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/integration/test_logs_api.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_alert_rules.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_cost_tracking.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_drift_detection.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_ingest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/synthetic/test_schema_validation.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/toy_agent/toy_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_backfill.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cache_efficacy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cache_recommend.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cmd_policy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_cmd_stop.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_compare.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_config.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_config_secret_divergence.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_demo_env.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_demo_scenarios.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_export_claude_code.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_formatting.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ingest_helicone.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ingest_langfuse.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_ingest_otlp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_litellm_client.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_litellm_integration.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_logs_converter.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_mcp_server.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_models.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_onboard_codex.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_onboard_daemon.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_openclaw_ingest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_optimize.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_pricing_override.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_prompt_bloat.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_spans_stats_repair.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_time_parse.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_transport_401.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tests/unit/test_workflow_restructure.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/deps.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/middleware.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/agents.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/logs.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/metrics.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/otlp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/spans.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/status.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/api/routes/tools.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_backfill.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_budget.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_doctor.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_export.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_mcp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_policy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_serve.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_status.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_stop.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_tools.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/cli/cmd_traces.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/backfill.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/config.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/export/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/export/claude_code.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/helicone.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/ingest_adapters/otlp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/optimize/registry.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/pricing.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/retention.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/core/schema_validator.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/demo/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/demo/env.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/mcp/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/exporters.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/otlp_parsing.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/provider.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/otel/semconv.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/py.typed +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/bootstrap.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/client.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/http_exporter.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/anthropic.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/autogen.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/base.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/bedrock.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/crewai.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/gemini.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/langchain.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/langgraph.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/litellm.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/llamaindex.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/openai.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/sdk/transport.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/ui/vendor/htm.js +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/ui/vendor/preact-hooks.js +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/ui/vendor/preact.js +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/formatting.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/ids.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.4.0}/tokenjam/utils/time_parse.py +0 -0
|
@@ -50,3 +50,7 @@ tj.toml
|
|
|
50
50
|
# Superpowers skill output (internal planning artifacts, not for OSS)
|
|
51
51
|
docs/superpowers/
|
|
52
52
|
.gstack/
|
|
53
|
+
|
|
54
|
+
# Per-release pre-release test logs are committed (for reference) — see
|
|
55
|
+
# tests/results/. They're small markdown files, one per release run, and
|
|
56
|
+
# serve as a record of what was verified at release time.
|
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
`tj` (TokenJam) is a local-first, OTel-native
|
|
7
|
+
`tj` (TokenJam) is a local-first, OTel-native **cost-optimization layer** for AI agents (with a full observability stack underneath). No cloud backend, no signup. It captures telemetry from agent runtimes, stores it in a local DuckDB database, and runs four named analyzers (`downsize` / `cache` / `script` / `trim`) that surface cost-saving candidates from real usage — plus a CLI, local REST API, web UI, and MCP server for querying. Install via `pipx install tokenjam` (recommended — sidesteps PEP 668 on Homebrew Python and Debian 12+/Ubuntu 24+) or `pip install tokenjam` in a venv. Run via `tj <subcommand>`. Requires Python >=3.10.
|
|
8
8
|
|
|
9
9
|
## Build & Development
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ pip install -e ".[dev]"
|
|
|
14
14
|
|
|
15
15
|
# Linting and type checking
|
|
16
16
|
ruff check tokenjam/ # line-length=100, target py310
|
|
17
|
-
mypy tokenjam/ # strict
|
|
17
|
+
mypy tokenjam/ # partial config (not --strict; see [tool.mypy] in pyproject.toml)
|
|
18
18
|
|
|
19
19
|
# Tests (CI runs all except e2e)
|
|
20
20
|
pytest tests/unit/ tests/synthetic/ tests/agents/ tests/integration/
|
|
@@ -37,6 +37,27 @@ cd sdk-ts && npm install && npm test
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
## Working with concurrent agents
|
|
41
|
+
|
|
42
|
+
When more than one agent is editing this repo in parallel, **each agent must operate in its own git worktree**. A single working directory shares one `HEAD`, so two `git commit` calls from different agents land on whichever branch was checked out last — leading to commits leaking into the wrong PR. We've hit this multiple times.
|
|
43
|
+
|
|
44
|
+
Spin up a per-task worktree before starting:
|
|
45
|
+
```bash
|
|
46
|
+
git worktree add ../tokenjam-<task> main
|
|
47
|
+
cd ../tokenjam-<task>
|
|
48
|
+
git checkout -b feat/<task>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
When the PR merges and the branch is deleted, prune the worktree:
|
|
52
|
+
```bash
|
|
53
|
+
git worktree remove ../tokenjam-<task>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Symptom of a missed worktree: `git log` shows a commit on a branch you didn't intend (because another agent's `HEAD` was the checked-out one when your `git commit` ran). If you see this, do **not** force-push — rebase the stray commit off your branch first, and only force-push if you own every commit being rewritten.
|
|
57
|
+
|
|
58
|
+
`.tj/config.toml` is intentionally untracked (see PR #145 + Critical Rule 20) and gets mutated at runtime by `tj onboard` / `tj serve` regenerating the local `ingest_secret`. Don't `git add` it back. The CI test `tests/unit/test_no_tracked_dev_secrets.py` guards against this.
|
|
59
|
+
|
|
60
|
+
|
|
40
61
|
## Architecture
|
|
41
62
|
|
|
42
63
|
### Data Flow
|
|
@@ -61,16 +82,25 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
61
82
|
- **`tokenjam/core/db.py`**: `StorageBackend` protocol + `DuckDBBackend` + `InMemoryBackend` (for tests) + migration runner. Migrations are `(version, sql)` tuples in a `MIGRATIONS` list — never modify existing ones, only append. **Note:** `StorageBackend` doesn't cover every query. Some callers (e.g. `CostEngine`, `cmd_status`) access `db.conn` directly for queries not in the protocol (cost updates, active session lookups). Helper `_row_to_session()` is used to convert raw DuckDB rows.
|
|
62
83
|
- **`tokenjam/core/ingest.py`**: `IngestPipeline` (central hub), `SpanSanitizer` (rejects oversized/malformed spans), `strip_captured_content()`. Post-ingest hooks (cost, alerts, schema) are optional and error-tolerant — hook failures are logged, never propagated.
|
|
63
84
|
- **`tokenjam/core/pricing.py`**: `ModelRates` (frozen dataclass), `load_pricing_table()` (LRU-cached), `get_rates(provider, model)`. Falls back to default rates for unknown models.
|
|
64
|
-
- **`tokenjam/core/cost.py`**: `calculate_cost()` (pure function, rounds to 8dp) + `CostEngine` (post-ingest hook that updates `spans.cost_usd` and `sessions.total_cost_usd` via `db.conn` — see db.py note). Pricing loaded from `pricing/models.toml`.
|
|
85
|
+
- **`tokenjam/core/cost.py`**: `calculate_cost()` (pure function, rounds to 8dp) + `CostEngine` (post-ingest hook that updates `spans.cost_usd` and `sessions.total_cost_usd` via `db.conn` — see db.py note). Pricing loaded from `tokenjam/pricing/models.toml`. **Cache-read vs cache-write are separate fields** on `NormalizedSpan` (`cache_tokens` = read, `cache_write_tokens` = create); they bill at different rates and `calculate_cost` charges each at its own rate. The early-return no-op guard checks all four token counts (input/output/cache_read/cache_write) — see PR #90 and PR #92 for the cache-only-span and cache-write-on-live-path fixes.
|
|
65
86
|
- **`tokenjam/core/alerts.py`**: `AlertEngine` with 13 alert types, `CooldownTracker` (in-memory, per agent+type, resets on restart), `AlertDispatcher` routing to 6 channel types (stdout, file, ntfy, webhook, Discord, Telegram). `AlertEngine.fire()` is the external entry point for other modules (SchemaValidator, DriftDetector) to fire alerts. Suppressed alerts are still persisted to DB but not dispatched to channels. Hardcoded thresholds: retry loop fires at 4+ identical tool calls in last 6 spans; failure rate fires at >20% errors in last 20 spans (checked every 5th error); session duration default 3600s. Stdout and file channels always include full detail regardless of `include_captured_content` config.
|
|
66
87
|
- **`tokenjam/core/drift.py`**: `DriftDetector` — Z-score based behavioral drift detection, fires at session end.
|
|
67
|
-
- **`tokenjam/core/optimize/`**: Package powering `tj optimize` and the `get_optimize_report` MCP tool. Public API re-exported from `__init__.py`: `build_report()` (orchestrator), `report_to_dict()`, `ANALYZER_REGISTRY`, `ANALYZER_ORDER`, plus result dataclasses. Architecture: `registry.py` holds the `@register("name")` decorator and `ANALYZER_REGISTRY` dict; `runner.py` defines `ANALYZER_ORDER` and orchestrates execution; `types.py` holds `AnalyzerContext` + result dataclasses + `MODEL_DOWNGRADE_CAVEAT`. Individual analyzers live in `analyzers/`, each as a single file registering via `@register
|
|
88
|
+
- **`tokenjam/core/optimize/`**: Package powering `tj optimize` and the `get_optimize_report` MCP tool. Public API re-exported from `__init__.py`: `build_report()` (orchestrator), `report_to_dict()`, `ANALYZER_REGISTRY`, `ANALYZER_ORDER`, plus result dataclasses. Architecture: `registry.py` holds the `@register("name")` decorator and `ANALYZER_REGISTRY` dict; `runner.py` defines `ANALYZER_ORDER` and orchestrates execution; `types.py` holds `AnalyzerContext` + result dataclasses + `MODEL_DOWNGRADE_CAVEAT`. Individual analyzers live in `analyzers/`, each as a single file registering via `@register`. **Registry strings (the user-facing names) and file names are decoupled**:
|
|
89
|
+
- `model_downgrade.py` → `@register("downsize")` — structural candidates (input < 5K tokens AND output < 500 tokens AND tool_calls ≤ 5; never claims quality equivalence, caveat baked into dataclass default)
|
|
90
|
+
- `budget_projection.py` → `@register("budget-projection")` — per-provider cycle spend vs `[budget.<provider>]` ceiling; only fires when budget > 0
|
|
91
|
+
- `cache_efficacy.py` → `@register("cache")` — current cache-read efficacy per (provider, model)
|
|
92
|
+
- `cache_recommend.py` → `@register("cache-recommend")` — Anthropic-only structural prefix detection for `cache_control` placement
|
|
93
|
+
- `workflow_restructure.py` → `@register("script")` — `(tool_name, arg_shape)` cluster detection for deterministic-script candidates
|
|
94
|
+
- `prompt_bloat.py` → `@register("trim")` — LLMLingua-2 token-significance classification (requires `tokenjam[bloat]` extra)
|
|
95
|
+
Analyzers receive an `AnalyzerContext` and operate on `db.conn` directly. To add a new analyzer: drop a file under `analyzers/`, decorate with `@register("name")`, append to `ANALYZER_ORDER` if ordering matters — `cmd_optimize`'s positional `findings` Click choices auto-derive from the registry.
|
|
96
|
+
**Recoverable-savings contract** (issues #111/#122): every *savings* analyzer's result dataclass carries `estimated_recoverable_usd` / `estimated_recoverable_tokens` / `estimate_basis` / `estimate_confidence` (`"heuristic"`). All four are on **one time basis — recoverable over the analyzed window** (`downsize` keeps a separate `monthly_savings_usd` for its CLI projection line, but `estimated_recoverable_usd` is the window figure so Overview tiles are comparable). `cache-recommend` and `budget-projection` deliberately carry **no** recoverable field (not savings analyzers); the Overview waste band is registry-driven off the presence of `estimated_recoverable_usd`, so a future analyzer (e.g. reuse) appears with no UI change. `report_to_dict`/`report_from_dict` round-trip these fields. Honesty discipline (Critical Rule 14) is mandatory — every estimate is "estimated recoverable", never "saves you".
|
|
68
97
|
- **`tokenjam/core/ingest_adapters/`**: Third-party trace-export adapters that normalize external payloads (`langfuse.py`, `helicone.py`, `otlp.py`) into `NormalizedSpan` for ingest. Each is reachable as a `tj backfill <name>` subcommand and accepts `--source-url` (live API) or `--source-file` (offline JSON dump). Adapters write deterministic span IDs derived from the source's identifiers so re-runs are idempotent. `otlp.py` shares span-mapping logic with the live `POST /api/v1/spans` route via `tokenjam/otel/otlp_parsing.py`.
|
|
69
98
|
- **`tokenjam/core/export/`**: Routing-config snippet generators for `tj optimize --export-config`. Currently `claude_code.py` emits a JSONC fragment under a `tokenjam.routing_recommendations` namespace with honest-framing caveat comments baked in. Writes to `~/.config/tokenjam/exports/`; never touches `~/.claude/settings.json` or other external configs (no `--apply` flag — Claude Code doesn't currently honor TokenJam routing keys, so auto-writing would change nothing and erode trust).
|
|
70
99
|
- **`tokenjam/core/backfill.py`**: Parses Claude Code on-disk session JSONL files into `NormalizedSpan`s. Cost is recomputed from `pricing/models.toml` because the on-disk format has no `cost_usd`. The parser tolerates the dated `claude-<family>-<ver>-YYYYMMDD` model-name suffixes Anthropic ships (handled by `core/pricing.py.get_rates()`, which strips the trailing 8-digit date suffix when no exact pricing match exists). Idempotency relies on deterministic span IDs derived from `(session_id, message uuid)` / `(session_id, tool_use id)`.
|
|
71
100
|
- **`tokenjam/core/schema_validator.py`**: Validates tool outputs against declared or genson-inferred JSON Schema. Only fires on `gen_ai.tool.call` spans with `gen_ai.tool.output` in attributes. Schema priority: 1) declared file from agent config `output_schema`, 2) inferred schema from `DriftBaseline.output_schema_inferred`. Caches schemas in-memory per agent.
|
|
72
101
|
- **`tokenjam/core/models.py`**: All domain dataclasses — `NormalizedSpan`, `SessionRecord`, `Alert`, `DriftBaseline`, filter types, etc. `NormalizedSpan` carries `billing_account` (provider-only: `anthropic` / `openai` / `google` / `bedrock` / `local.ollama`). `SessionRecord` carries `plan_tier` (api / pro / max_5x / max_20x / plus / team / enterprise / local / unknown) plus a derived `pricing_mode` property (`local` / `subscription` / `api` / `unknown`). Spans inherit plan via the session FK — analyzers JOIN through `SessionRecord` when they need plan context. See [`docs/architecture.md`](docs/architecture.md) → "OTel semconv extensions" for the full derivation rules.
|
|
73
102
|
- **`tokenjam/core/config.py`**: `TjConfig` dataclass tree, TOML loading/writing, config file discovery. `ProviderBudget` carries an optional `plan` field (set by `tj onboard`'s plan-tier prompt) that `IngestPipeline._build_or_update_session` reads to populate `SessionRecord.plan_tier` at session creation. `CaptureConfig` has four fine-grained content-capture toggles (`prompts` / `completions` / `tool_inputs` / `tool_outputs`); `strip_captured_content()` in `core/ingest.py` enforces them at the single ingest-pipeline gate.
|
|
103
|
+
- **`tokenjam/core/framing.py`**: **Single source of truth for plan-tier-aware rendering** (issue #110). `compute_framing(config, window_summary, by_provider_breakdown) -> Framing` decides whether dollar figures are shown verbatim (`api`), suppressed for token-share framing (`subscription`), shown as tokens-only (`local`), or shown with an "may overstate" qualifier (`unknown`). Plus `render_dollar()` / `render_savings()` (UI-facing compact formatters), and the shared helpers `pricing_mode_for` / `dominant_plan` / `config_declared_plan` (with the #106 global-config fallback) / `plan_tier_mix`. **Consumed by both the CLI (`cmd_optimize`, `cmd_tokenmaxx`) and the REST API** (which emits `Framing.to_dict()` as the `framing` block) — neither re-derives the rules. This module *reads* plan-tier/pricing-mode; the canonical derivation still lives on `SessionRecord.pricing_mode` + `SUBSCRIPTION_PLAN_TIERS` (semconv). When adding a dollar-bearing surface, consume this — do not re-implement the suppression rules.
|
|
74
104
|
- **`tokenjam/sdk/agent.py`**: `@watch()` decorator creates session spans only. `record_llm_call()` and `record_tool_call()` create child spans for manual instrumentation. LLM call spans from provider clients require `patch_anthropic()`, `patch_openai()`, etc.
|
|
75
105
|
- **`tokenjam/sdk/transport.py`**: `HttpTransport` — buffers up to 1000 spans, retries with exponential backoff (3 attempts, 2s base). Used when `tj serve` runs as a separate process.
|
|
76
106
|
- **`tokenjam/sdk/bootstrap.py`**: `ensure_initialised()` — lazy, thread-safe, idempotent bootstrap of config -> DB -> IngestPipeline -> TracerProvider. Called automatically by `@watch()` and all `patch_*()` functions. Registers atexit flush.
|
|
@@ -79,10 +109,10 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
79
109
|
- **`tokenjam/otel/exporters.py`**: Prometheus metric reader setup via `build_prometheus_exporter()`.
|
|
80
110
|
- **`tokenjam/otel/otlp_parsing.py`**: Shared OTLP JSON → `NormalizedSpan` parser. Two callers: `api/routes/spans.py` (live `POST /api/v1/spans`) and `core/ingest_adapters/otlp.py` (`tj backfill otlp`). Keep parsing in this one place — the live receive path and the backfill adapter must agree on attribute extraction, billing_account derivation, and timestamp handling.
|
|
81
111
|
- **`tokenjam/otel/semconv.py`**: `GenAIAttributes`, `TjAttributes` (includes `BILLING_ACCOUNT` and `PLAN_TIER`), `VALID_PLAN_TIERS` and `SUBSCRIPTION_PLAN_TIERS` frozensets — OTel GenAI semantic convention constants plus tj-specific extensions.
|
|
82
|
-
- **`tokenjam/api/app.py`**: FastAPI app factory. `tj serve` starts it with uvicorn. Accepts `db`, `config`, `ingest_pipeline` for testability. Registers all routers under `/api/v1` plus `/metrics`.
|
|
112
|
+
- **`tokenjam/api/app.py`**: FastAPI app factory (OpenAPI title `"TokenJam Lens"`). `tj serve` starts it with uvicorn. Accepts `db`, `config`, `ingest_pipeline` for testability. Registers all routers under `/api/v1` plus `/metrics`, `/health`, and the SPA at `/`. **`index.html` is read into a module string once at `create_app()` time** (`_index_html`) — so editing `tokenjam/ui/index.html` requires a `tj serve` restart to take effect; tests read the file from disk directly and aren't affected. Mounts `/ui/vendor` as `StaticFiles`.
|
|
83
113
|
- **`tokenjam/api/middleware.py`**: `IngestAuthMiddleware` — protects `POST /api/v1/spans` with Bearer token. Returns `JSONResponse(401)` directly (not `HTTPException`, which doesn't propagate from `BaseHTTPMiddleware.dispatch`).
|
|
84
114
|
- **`tokenjam/api/deps.py`**: `require_api_key` — FastAPI dependency for optional API key auth on GET endpoints. Only enforced when `api.auth.enabled = true` in config.
|
|
85
|
-
- **`tokenjam/api/routes/`**: One file per resource — `spans.py` (OTLP JSON ingest), `traces.py`, `cost.py`, `tools.py`, `alerts.py`, `drift.py`, `metrics.py` (Prometheus text format from DB queries).
|
|
115
|
+
- **`tokenjam/api/routes/`**: One file per resource — `spans.py` (OTLP JSON ingest), `traces.py`, `cost.py`, `cost_compare.py`, `tools.py`, `alerts.py`, `drift.py`, `optimize.py`, `budget.py`, `status.py`, `agents.py`, `metrics.py` (Prometheus text format from DB queries), `version.py` (unauthenticated `GET /health` → `{"status":"ok","version":...}` mounted with no prefix, plus `GET /api/v1/version`; the version is derived at runtime via `importlib.metadata.version("tokenjam")` — no hardcoded literal). **The dollar-bearing read routes (`/cost`, `/cost/compare`, `/optimize`, `/budget`) each return a `framing` block** (see `core/framing.py`) so the web UI renders plan-tier-aware figures without re-deriving the rules in JS. `/optimize` takes `?fast=true` to skip the expensive Trim analyzer (returns `skipped_analyzers`) for the polling Overview; `/cost` returns a window-bucketed `series` for the chart (see Web UI below). **Concurrency caveat:** the sync (`def`) read routes run in Starlette's threadpool and share the daemon's single DuckDB connection, which is not safe for concurrent use — fan-out callers (the Overview) must fetch sequentially. See issue #124.
|
|
86
116
|
- **`tokenjam/mcp/server.py`**: FastMCP stdio server exposing observability data to Claude Code. Uses either a read-only DuckDB connection or HTTP proxy to `tj serve`. Initialized via `init()` from `cmd_mcp.py`.
|
|
87
117
|
- **`tokenjam/cli/main.py`**: Root Click group with global options (`--config`, `--json`, `--no-color`, `--db`, `--agent`, `-v`). Registers all subcommands.
|
|
88
118
|
|
|
@@ -92,11 +122,12 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
92
122
|
|
|
93
123
|
- **`tj demo [scenario]`** (`cmd_demo.py`) — runs Agent Incident Library scenarios (zero-config, no API keys). `tj demo` lists all; `tj demo retry-loop` runs one.
|
|
94
124
|
- **`tj doctor`** (`cmd_doctor.py`) — health checks (config, DB, secrets, webhooks, drift readiness, schema-vs-capture consistency). Exit 0 = ok, 1 = warnings, 2 = errors.
|
|
95
|
-
- **`tj optimize`** (`cmd_optimize.py`) — six analyzers, registry-driven: `
|
|
125
|
+
- **`tj optimize`** (`cmd_optimize.py`) — six analyzers, registry-driven. **Analyzers are positional args** (not `--finding <name>`): `tj optimize downsize cache trim` runs three; bare `tj optimize` runs all. Registered names: `downsize`, `cache`, `cache-recommend`, `script`, `trim`, `budget-projection`. Flags: `--since 30d`, `--budget <provider>`, `--budget-usd <amount>`, `--compare <period>` (window-cost diff vs prior period; accepts `previous` / `last-week` / `last-month` / `last-7d` / `last-30d` / `YYYY-MM-DD:YYYY-MM-DD`), `--export-config <target>` (writes a routing snippet — currently `claude-code` — under `~/.config/tokenjam/exports/`; no `--apply` flag by design). Plan-tier-aware rendering: subscription users see "implied API value" framing and token-share savings (never dollar "spend"); local users see token-only framing; unknown-plan users see dollar figures suppressed with a `tj onboard --reconfigure` hint. Works alongside a running `tj serve` via the `/api/v1/optimize` HTTP fallback when the DuckDB write lock is held by the daemon.
|
|
126
|
+
- **`tj tokenmaxx`** (`cmd_tokenmaxx.py`) — shareable spend-tier command. Reads last 30 days of usage, classifies into a 6-tier ladder (Sipper / Moderator / Maxxer / SuperMaxxer / MegaMaxxer / GigaMaxxer) using the multiplier vs the user's declared subscription plan as the primary classifier, with absolute USD/mo thresholds as the API-user fallback. Output is a bordered Panel designed for screenshotting. Plan-aware: shows the multiplier line only when the user has `[budget.<provider>] plan = "max_5x"` (or pro / max_20x / plus) configured — the declared-plan lookup uses `core/framing.config_declared_plan`, which falls back to the global `~/.config/tj/config.toml` when the active project config has no `[budget]` section (issue #106). The companion landing page is `tokenjam.dev/tokenmaxxing`. Designed to never exit without an actionable next step — pairs the tier callout with the downsize savings figure inline.
|
|
96
127
|
- **`tj cost`** (`cmd_cost.py`) — cost breakdown by `--group-by agent|model|day|tool`. Same `--compare <period>` flag as `tj optimize` for window-over-window diffs (▲/▼ indicators, per-agent and per-model top-shifts, dollar + token deltas).
|
|
97
128
|
- **`tj backfill <source>`** (`cmd_backfill.py`) — ingest historical telemetry from external sources. Subcommands: `claude-code` (parses `~/.claude/projects/*.jsonl`, auto-invoked at the end of `tj onboard --claude-code`), `langfuse` (live API or JSON dump), `helicone` (live API or JSON dump), `otlp` (raw OTLP JSON via URL or file — reuses the same parser as the live `POST /api/v1/spans` route). All idempotent via deterministic span IDs.
|
|
98
|
-
- **`tj onboard`** (`cmd_onboard.py`) — `--claude-code` and `--codex` flags trigger integration-specific flows.
|
|
99
|
-
- **`tj report`** (`cmd_report.py`) — generates standalone HTML visualizations of analyzer findings
|
|
129
|
+
- **`tj onboard`** (`cmd_onboard.py`) — `--claude-code` and `--codex` flags trigger integration-specific flows (writing to the **global** config). All paths — including plain `tj onboard` — prompt for plan tier (api / pro / max_5x / max_20x for Anthropic; api / plus / team / enterprise for OpenAI) and write it to `[budget.<provider>] plan = "..."`; `--plan <tier>` sets it non-interactively (issue #4). The plain path is Claude-first: its interactive prompt offers the Anthropic tiers, and an OpenAI-only `--plan` (plus/team/enterprise) is routed to `[budget.openai]`. Supports `--reconfigure` to re-prompt against an existing config. Does NOT auto-write a default `usd = 200` cycle ceiling — subscription users get only the `plan` field; API users are explicitly asked whether they want a self-imposed ceiling.
|
|
130
|
+
- **`tj report`** (`cmd_report.py`) — generates standalone HTML visualizations of analyzer findings. Currently `tj report --trim [<agent_id>]` renders the Trim analyzer's per-token significance (was `--bloat` pre-0.3.1, renamed alongside the analyzer's registry string). Writes to `~/.cache/tokenjam/reports/` (override via `TOKENJAM_REPORT_DIR`) and opens in the default browser.
|
|
100
131
|
- **`tj policy list`** (`cmd_policy.py`) — read-only preview of the unified policy surface. Consolidates existing `[alerts]`, `[alerts.channels]`, `[defaults.budget]`, `[budget.<provider>]`, per-agent `budget`/`drift`/`sensitive_actions`/`output_schema`, and `[capture]` config into one table; each row carries its source TOML section. Supports `--json`. `tj policy add | edit | apply | remove | test` are intentionally absent this sprint — the unified config migration is next sprint's work. `policy` is in `no_db_commands` in `cli/main.py` so it doesn't open the DB. Rich source-section strings (`[budget.anthropic]`, `[[alerts.channels]]`) must be passed through `rich.markup.escape()` before rendering — otherwise Rich consumes them as style tags.
|
|
101
132
|
|
|
102
133
|
All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
|
|
@@ -121,6 +152,18 @@ For `GET /api/v1/drift`, if `agent_id` is missing, return `JSONResponse(status_c
|
|
|
121
152
|
|
|
122
153
|
Integration tests use `httpx.AsyncClient` with `httpx.ASGITransport(app=app)` against `InMemoryBackend`. Synthetic alert tests use `unittest.mock.MagicMock` for the DB — you must explicitly set up `db.get_recent_spans.return_value` before calling `engine.evaluate()`, and silence channels with `engine.dispatcher.channels = []`.
|
|
123
154
|
|
|
155
|
+
### Web UI ("TokenJam Lens")
|
|
156
|
+
|
|
157
|
+
`tokenjam/ui/index.html` is the served dashboard — a **single-file Preact + htm SPA** (no build step, no TypeScript, no client-side router). "TokenJam Lens" is the **brand only**: it appears in `<title>`, the sidebar wordmark, and the OpenAPI title, but never in module names, route paths, or config keys. Screens: **Overview** (the default landing route — a triage front door), Status, Traces, Cost, Alerts, Drift, Optimize, Budget.
|
|
158
|
+
|
|
159
|
+
- **Offline-first (Critical Rule 18):** every JS/CSS dep is vendored under `tokenjam/ui/vendor/` — Preact + hooks + htm (ESM via `<script type="importmap">`) and **uPlot** (vendored IIFE global `uPlot` + CSS, pinned in `docs/internal/lens-vendor-versions.md`). No render-time external HTTP. `tests/unit/test_ui_offline.py` enforces this; clickable `<a href>` links are the only allowed external URLs.
|
|
160
|
+
- **Single compute path:** the UI reads everything from the REST API and **never re-implements analysis, aggregation, or plan-tier framing in JS** — it consumes the `framing` block (see `core/framing.py`). If the UI needs a number, extend the endpoint; don't compute it client-side.
|
|
161
|
+
- **URL is the source of truth for filters:** state lives in the hash + query params (`#/cost?since=7d&group_by=model`); `getRoute()` parses it, `navigate()` writes it back omitting defaults. Window vocabulary matches the CLI (`1h`/`24h`/`7d`/`30d`/`90d` + `YYYY-MM-DD:YYYY-MM-DD`). The default landing route is Overview (empty hash → `getRoute()` returns `overview`; do **not** re-introduce a render-time `location.hash = ...` redirect — it raced the first render, issue #132).
|
|
162
|
+
- **Charts:** `SpendChart` wraps uPlot, reads CSS custom properties (`--chart-1..5`) so it re-themes, and has a cursor tooltip. The spend chart spans the **full selected window** with zero-fill: `/api/v1/cost` returns a window-bucketed `series` (hourly buckets for ≤2-day windows, daily otherwise; epoch-second `bucket` keys) plus `series_bucket` + `window_start`/`window_end`, and the UI builds a continuous grid + pins the x-scale to the window (issues #133/#136).
|
|
163
|
+
- **Run-rate** is a single linear figure projected to the end of the current calendar cycle (`daily_rate × days-remaining`), captioned "not a forecast". The forecasting boundary is deliberate: linear run-rate only — no EWMA, seasonality, or anomaly detection.
|
|
164
|
+
- **Polling:** the Overview auto-refreshes every 30s only while the tab is visible (`document.visibilityState`) and **fetches its endpoints sequentially** (the daemon's single DuckDB connection isn't concurrency-safe — see the REST API caveat / #124). Detail screens refresh on user action.
|
|
165
|
+
- **Testing the UI (no JS runner in CI):** the Python `test` job can't run JS, so UI fixes are guarded by **static-grep regression tests** in `tests/unit/test_lens_ui_regression.py` (assert buggy patterns are *gone* and new helpers are present) plus `test_ui_offline.py`. When iterating locally, validate syntax with `node --check` on the extracted `<script type="module">` block, and verify visually by running `tj serve` (or a seeded `create_app` + uvicorn on an alt port) and screenshotting with headless Chrome — there is intentionally no Playwright/Cypress.
|
|
166
|
+
|
|
124
167
|
### Session Continuity
|
|
125
168
|
|
|
126
169
|
When a span has a `conversation_id` matching an existing session, it's attributed to that session (even across process restarts). New `conversation_id` = new session.
|
|
@@ -139,11 +182,14 @@ When a span has a `conversation_id` matching an existing session, it's attribute
|
|
|
139
182
|
10. **Use semconv constants** — reference `GenAIAttributes` and `TjAttributes` from `tokenjam/otel/semconv.py` instead of hardcoding OTel attribute name strings.
|
|
140
183
|
11. **OTel TracerProvider is global and set-once** — `trace.set_tracer_provider()` only works once per process. In tests, set the provider once at module level (not per-test in a fixture) and clear spans between tests. Use a custom `_CollectingExporter(SpanExporter)` since `InMemorySpanExporter` is not available in the installed OTel version. See `tests/agents/test_mock_scenarios.py` for the SDK test pattern and `tests/integration/test_full_pipeline.py` for the pipeline pattern.
|
|
141
184
|
12. **New SDK integrations must call `ensure_initialised()`** — every `patch_*()` convenience function must call `from tokenjam.sdk.bootstrap import ensure_initialised; ensure_initialised()` before installing hooks. This lazily bootstraps the TracerProvider + IngestPipeline on first use.
|
|
142
|
-
13. **PyPI package name is `tokenjam`, not `ocw`** —
|
|
143
|
-
14. **`tj optimize` output must never claim quality equivalence** — the
|
|
185
|
+
13. **PyPI package name is `tokenjam`, not `ocw`** — the package on PyPI is `tokenjam`. The CLI command is `tj`. The Python package directory is `tokenjam/`. **Recommended install: `pipx install tokenjam`** (sidesteps PEP 668 on Homebrew Python and Debian 12+/Ubuntu 24+). `pip install tokenjam` works inside a clean venv but fails on system Python with a misleading externally-managed-environment error. Never write `pip install ocw` in docs, examples, or comments.
|
|
186
|
+
14. **`tj optimize` output must never claim quality equivalence** — the `downsize` finding flags structural candidates only. Every user-visible string says "looks like" / "candidate" / "review before switching" — never "safe to downgrade" or "would have worked." The `MODEL_DOWNGRADE_CAVEAT` constant lives on `DowngradeFinding` as a dataclass default so it can't be removed by accident; it must also appear in human-readable CLI output. The same honesty discipline applies to all other analyzers — `cache` ("you're getting X% of available caching"), `cache-recommend` (Anthropic-only, structural prefix detection), `script` ("structural shape matches", "review before replacing with a script"), `trim` ("predicted low-significance regions; review before editing"). `tj optimize --export-config` snippets bake the caveat block into the JSONC output as comments.
|
|
144
187
|
15. **Version bump on release** — both `pyproject.toml` (`version = "X.Y.Z"`) and `sdk-ts/package.json` (`"version": "X.Y.Z"`) must be bumped to the new version before creating a GitHub release. The publish workflows (`publish-pypi.yml`, `publish-npm.yml`) trigger on `release published` events and will fail with 403 if the version already exists on PyPI/npm.
|
|
145
|
-
16. **New optimize analyzers self-register** — drop a `.py` file under `tokenjam/core/optimize/analyzers/` with a function decorated `@register("name")` taking `AnalyzerContext`. Auto-discovery in `analyzers/__init__.py` walks the directory at import time. `cmd_optimize.py`'s
|
|
188
|
+
16. **New optimize analyzers self-register** — drop a `.py` file under `tokenjam/core/optimize/analyzers/` with a function decorated `@register("name")` taking `AnalyzerContext`. Auto-discovery in `analyzers/__init__.py` walks the directory at import time. `cmd_optimize.py`'s positional `findings` Click choices read from `ANALYZER_REGISTRY.keys()` at decoration — no edits needed there. If your analyzer depends on (or is depended on by) another, append it to `ANALYZER_ORDER` in `runner.py` at the right position. Wave-2 analyzers attach their findings to `OptimizeReport.findings[name]` (generic dict); the older `downsize` (registered name; file is `model_downgrade.py`) and `budget-projection` analyzers retain typed slots on `OptimizeReport` for backwards compat with `cmd_optimize` and the MCP server.
|
|
146
189
|
17. **OTLP parsing has one home** — `tokenjam/otel/otlp_parsing.py`. Both the live `POST /api/v1/spans` route and the `tj backfill otlp` adapter import `parse_otlp_span` and `extract_resource_attrs` from there. If you need to extend OTLP attribute extraction, do it once in that module; do not copy-paste into either caller.
|
|
190
|
+
18. **Web UI must work fully offline** — `tokenjam/ui/index.html` is the served dashboard ("TokenJam Lens"; see Architecture → Web UI). It is intentionally a single-file SPA with **zero external HTTP loads at render time**. Preact + hooks + htm + **uPlot** are vendored under `tokenjam/ui/vendor/` (ESM via `<script type="importmap">`; uPlot as a plain `<script>` IIFE global); fonts use system-font fallbacks (no Google Fonts); the favicon is inlined as a `data:` URL. The FastAPI app mounts `/ui/vendor` as `StaticFiles`. The `tests/unit/test_ui_offline.py` regression test asserts no render-time external URLs exist anywhere outside `<a href>` (clickable links to github.com are fine — they only fetch on click) and that vendored CSS has no external `url()`. If you add a CDN font, script, or stylesheet, that test will fail. Vendor the asset locally instead. See issue #87 + PR #88.
|
|
191
|
+
19. **Analyzer registry names ≠ file names** — registry strings (`downsize`, `cache`, `script`, `trim`) are decoupled from Python module filenames (`model_downgrade.py`, `cache_efficacy.py`, `workflow_restructure.py`, `prompt_bloat.py`). The 0.3.1 rename only changed `@register("...")` strings; file names stayed for git-blame continuity. When grepping for an analyzer, search both the registry string AND the older file-name keyword.
|
|
192
|
+
20. **`.tj/config.toml` is untracked and must stay that way** — the file contains a live per-install `ingest_secret` and is regenerated by `tj onboard` / `tj serve`. It was committed in error from v0.2.0 through v0.3.5 (leaked secret in git history; see PR #145 + issue #141 finding #6). `.gitignore` covers it, and `tests/unit/test_no_tracked_dev_secrets.py` fails CI if it's re-added to the index. If you see `.tj/config.toml` in your `git status` as modified or new, that's expected — just don't `git add` it.
|
|
147
193
|
|
|
148
194
|
## Config
|
|
149
195
|
|
|
@@ -217,7 +263,7 @@ If a version already exists on PyPI or npm, the publish workflow fails with 403
|
|
|
217
263
|
|
|
218
264
|
## Packaging
|
|
219
265
|
|
|
220
|
-
Build system is hatchling.
|
|
266
|
+
Build system is hatchling. `[tool.hatch.build.targets.wheel] packages = ["tokenjam"]` — the package directory is `tokenjam/` (matching the PyPI name); only the *CLI command* is `tj` (`[project.scripts] tj = "tokenjam.cli.main:cli"`). Non-`.py` assets under the package ship in the wheel automatically — this is how the vendored UI (`tokenjam/ui/index.html`, `tokenjam/ui/vendor/*`) and `tokenjam/pricing/models.toml` reach users.
|
|
221
267
|
|
|
222
268
|
Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column handling — it's listed explicitly in `dependencies` because DuckDB doesn't declare it on all platforms.
|
|
223
269
|
|
|
@@ -233,10 +279,11 @@ Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column ha
|
|
|
233
279
|
- **[docs/installation.md](docs/installation.md)** — base install vs optional extras matrix. Documents `tokenjam[bloat]` (the ~2GB torch + transformers extra used by the Trim analyzer), framework adapter extras (`[langchain]` / `[crewai]` / `[autogen]` / `[litellm]`), and the MCP / dev extras.
|
|
234
280
|
- **[docs/configuration.md](docs/configuration.md)** — full TOML config surface plus the "Content capture and privacy" section explaining the four `[capture]` toggles and how they interact with `alerts.include_captured_content`.
|
|
235
281
|
- **Optimize product pages** — one per user-facing product, all under `docs/optimize/`:
|
|
236
|
-
- [`downsize.md`](docs/optimize/downsize.md) — model
|
|
237
|
-
- [`cache.md`](docs/optimize/cache.md) — `cache
|
|
238
|
-
- [`script.md`](docs/optimize/script.md) — `
|
|
239
|
-
- [`trim.md`](docs/optimize/trim.md) — LLMLingua-2 token-significance classifier (`
|
|
282
|
+
- [`downsize.md`](docs/optimize/downsize.md) — cheaper-model candidate flagging (registry: `downsize`, file: `model_downgrade.py`)
|
|
283
|
+
- [`cache.md`](docs/optimize/cache.md) — `cache` (current caching ratio) + `cache-recommend` (Anthropic-only breakpoint suggestions)
|
|
284
|
+
- [`script.md`](docs/optimize/script.md) — `script` clustering by `(tool_name, arg_shape)` signature (file: `workflow_restructure.py`)
|
|
285
|
+
- [`trim.md`](docs/optimize/trim.md) — LLMLingua-2 token-significance classifier (`trim`, file: `prompt_bloat.py`), install + capture requirements, performance numbers
|
|
286
|
+
- **[AGENTS.md](AGENTS.md)** — codebase conventions for contributors (referenced from the top-level README).
|
|
240
287
|
- **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.
|
|
241
288
|
- **[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.
|
|
242
289
|
- **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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenjam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -23,6 +23,7 @@ Requires-Dist: apscheduler>=3.10
|
|
|
23
23
|
Requires-Dist: click>=8.1
|
|
24
24
|
Requires-Dist: duckdb>=0.10
|
|
25
25
|
Requires-Dist: fastapi>=0.110
|
|
26
|
+
Requires-Dist: fastmcp>=0.2
|
|
26
27
|
Requires-Dist: genson>=1.2
|
|
27
28
|
Requires-Dist: httpx>=0.27
|
|
28
29
|
Requires-Dist: jsonschema>=4.0
|
|
@@ -53,7 +54,6 @@ Requires-Dist: langchain>=0.2; extra == 'langchain'
|
|
|
53
54
|
Provides-Extra: litellm
|
|
54
55
|
Requires-Dist: litellm>=1.40; extra == 'litellm'
|
|
55
56
|
Provides-Extra: mcp
|
|
56
|
-
Requires-Dist: fastmcp; extra == 'mcp'
|
|
57
57
|
Description-Content-Type: text/markdown
|
|
58
58
|
|
|
59
59
|
<div align="center">
|
|
@@ -154,6 +154,8 @@ tj onboard --claude-code
|
|
|
154
154
|
tj optimize # cost-saving candidates from your actual usage
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
To upgrade later: `pipx upgrade tokenjam` (then `tj stop && tj serve &` to reload the daemon, and `tj --version` to verify). See [docs/installation.md](docs/installation.md#upgrading).
|
|
158
|
+
|
|
157
159
|
For any Python agent:
|
|
158
160
|
|
|
159
161
|
```python
|
|
@@ -259,6 +261,8 @@ tj serve # start the web UI + REST API
|
|
|
259
261
|
**Shipped in 0.3.x:** Downsize · Cache · Script · Trim · Claude Code + Codex onboarding · MCP server · Web UI · Backfill adapters (Langfuse, Helicone, OTLP) · Period comparison · Routing-config export · Read-only policy preview
|
|
260
262
|
|
|
261
263
|
**Up next:**
|
|
264
|
+
- [ ] **[TokenJam Lens](https://github.com/Metabuilder-Labs/tokenjam/milestone/1)** — local dashboard rebrand: new Overview triage front-door, Optimize detail tab, real spend-over-time charts, cross-screen drill-through
|
|
265
|
+
- [ ] **[Reuse analyzer](https://github.com/Metabuilder-Labs/tokenjam/milestone/2)** — fifth analyzer: detects clusters of sessions with repeated planning, exports reviewable skeleton templates you can convert into slash commands or scripts
|
|
262
266
|
- [ ] `tj policy add | edit | apply` — unified rule surface
|
|
263
267
|
- [ ] `tj replay` — replay captured sessions against new model versions
|
|
264
268
|
- [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
|
|
@@ -96,6 +96,8 @@ tj onboard --claude-code
|
|
|
96
96
|
tj optimize # cost-saving candidates from your actual usage
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
To upgrade later: `pipx upgrade tokenjam` (then `tj stop && tj serve &` to reload the daemon, and `tj --version` to verify). See [docs/installation.md](docs/installation.md#upgrading).
|
|
100
|
+
|
|
99
101
|
For any Python agent:
|
|
100
102
|
|
|
101
103
|
```python
|
|
@@ -201,6 +203,8 @@ tj serve # start the web UI + REST API
|
|
|
201
203
|
**Shipped in 0.3.x:** Downsize · Cache · Script · Trim · Claude Code + Codex onboarding · MCP server · Web UI · Backfill adapters (Langfuse, Helicone, OTLP) · Period comparison · Routing-config export · Read-only policy preview
|
|
202
204
|
|
|
203
205
|
**Up next:**
|
|
206
|
+
- [ ] **[TokenJam Lens](https://github.com/Metabuilder-Labs/tokenjam/milestone/1)** — local dashboard rebrand: new Overview triage front-door, Optimize detail tab, real spend-over-time charts, cross-screen drill-through
|
|
207
|
+
- [ ] **[Reuse analyzer](https://github.com/Metabuilder-Labs/tokenjam/milestone/2)** — fifth analyzer: detects clusters of sessions with repeated planning, exports reviewable skeleton templates you can convert into slash commands or scripts
|
|
204
208
|
- [ ] `tj policy add | edit | apply` — unified rule surface
|
|
205
209
|
- [ ] `tj replay` — replay captured sessions against new model versions
|
|
206
210
|
- [ ] TypeScript framework patches (LangChain JS, OpenAI Agents SDK)
|
|
@@ -74,6 +74,21 @@ If you run `tj optimize trim` without the extra installed, the analyzer self-reg
|
|
|
74
74
|
|
|
75
75
|
See [`docs/optimize/trim.md`](optimize/trim.md) for performance numbers, capture requirements, and what the analyzer actually reports.
|
|
76
76
|
|
|
77
|
+
## Upgrading
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pipx upgrade tokenjam # if you installed via pipx (recommended)
|
|
81
|
+
pip install --upgrade tokenjam # if you're in a pip + venv setup
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
After upgrading:
|
|
85
|
+
|
|
86
|
+
1. Restart the daemon to pick up the new code: `tj stop && tj serve &`
|
|
87
|
+
2. DB migrations apply automatically on the next `tj` invocation — no manual step required
|
|
88
|
+
3. Verify with `tj --version`
|
|
89
|
+
|
|
90
|
+
PyPI's CDN occasionally lags ~1–2 min after a release. If `pipx upgrade` reports "already at the latest version" but the reported `tj --version` is older than what's on the [releases page](https://github.com/Metabuilder-Labs/tokenjam/releases), wait a minute and retry.
|
|
91
|
+
|
|
77
92
|
## TypeScript SDK
|
|
78
93
|
|
|
79
94
|
```bash
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Vendored front-end libraries
|
|
2
|
+
|
|
3
|
+
The local web UI (`tokenjam/ui/index.html`) is offline-first (CLAUDE.md Critical
|
|
4
|
+
Rule 18): every JS/CSS dependency is vendored under `tokenjam/ui/vendor/` and
|
|
5
|
+
served by the FastAPI `/ui/vendor` StaticFiles mount. No CDN loads at render
|
|
6
|
+
time. One line per vendored library below.
|
|
7
|
+
|
|
8
|
+
| Library | Version | Files | License | Notes |
|
|
9
|
+
|---|---|---|---|---|
|
|
10
|
+
| Preact | (as vendored) | `preact.js`, `preact-hooks.js` | MIT | ESM, via importmap |
|
|
11
|
+
| htm | (as vendored) | `htm.js` | Apache-2.0 | ESM, via importmap |
|
|
12
|
+
| uPlot | 1.6.32 | `uplot.js`, `uplot.css` | MIT | IIFE global `uPlot`, loaded via plain `<script>` (issue #112) |
|
|
13
|
+
|
|
14
|
+
## Bump procedure
|
|
15
|
+
|
|
16
|
+
1. Download the new release's `dist/` files from the upstream repo
|
|
17
|
+
(uPlot: `dist/uPlot.iife.min.js` + `dist/uPlot.min.css`).
|
|
18
|
+
2. Replace the file under `tokenjam/ui/vendor/`, keeping the version-pin header
|
|
19
|
+
comment at the top.
|
|
20
|
+
3. Update the version in the table above.
|
|
21
|
+
4. Run `pytest tests/unit/test_ui_offline.py` — it asserts no render-time
|
|
22
|
+
external URLs and that the vendored files exist and ship in the wheel.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tokenjam"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "TokenJam — local-first OTel-native observability for Autonomous AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -41,6 +41,11 @@ dependencies = [
|
|
|
41
41
|
"httpx>=0.27",
|
|
42
42
|
"apscheduler>=3.10",
|
|
43
43
|
"websockets>=12.0",
|
|
44
|
+
# fastmcp ships in the base install (was in the [mcp] extra) so `tj mcp`
|
|
45
|
+
# works on a fresh `pipx install tokenjam` without requiring users to
|
|
46
|
+
# remember the extra. Claude Code's MCP integration is now a primary
|
|
47
|
+
# use case rather than an opt-in. Issue #101.
|
|
48
|
+
"fastmcp>=0.2",
|
|
44
49
|
]
|
|
45
50
|
|
|
46
51
|
[project.urls]
|
|
@@ -54,7 +59,10 @@ crewai = ["crewai>=0.28"]
|
|
|
54
59
|
autogen = ["pyautogen>=0.2"]
|
|
55
60
|
litellm = ["litellm>=1.40"]
|
|
56
61
|
dev = ["pytest", "pytest-asyncio", "httpx", "ruff", "mypy"]
|
|
57
|
-
|
|
62
|
+
# Kept as a no-op extra for back-compat — `pipx install 'tokenjam[mcp]'` still
|
|
63
|
+
# works, just installs the same fastmcp that's now in the base dependencies.
|
|
64
|
+
# Documented in `docs/installation.md` so users know they no longer need it.
|
|
65
|
+
mcp = []
|
|
58
66
|
# Trim analyzer (`tj optimize --finding prompt-bloat`). LLMLingua-2 pulls in
|
|
59
67
|
# PyTorch and transformers, ~2GB total. Kept optional so the base install
|
|
60
68
|
# stays small — most users don't run the bloat analyzer.
|
|
@@ -238,6 +238,154 @@ async def test_get_cost_returns_aggregated_rows(client):
|
|
|
238
238
|
assert "total_cost_usd" in data
|
|
239
239
|
|
|
240
240
|
|
|
241
|
+
async def test_trace_detail_includes_cache_write_tokens(db, client):
|
|
242
|
+
"""The traces API exposes cache_write_tokens so per-span cost reconciles
|
|
243
|
+
from the displayed columns (#17 — it was the ~91% cost driver, hidden)."""
|
|
244
|
+
sp = make_llm_span(agent_id="a", model="claude-opus-4-8", provider="anthropic",
|
|
245
|
+
input_tokens=2, output_tokens=465, cache_tokens=243597,
|
|
246
|
+
cache_write_tokens=209000, cost_usd=1.4423)
|
|
247
|
+
db.insert_span(sp)
|
|
248
|
+
resp = await client.get(f"/api/v1/traces/{sp.trace_id}")
|
|
249
|
+
assert resp.status_code == 200
|
|
250
|
+
span = resp.json()["spans"][0]
|
|
251
|
+
assert span["cache_tokens"] == 243597
|
|
252
|
+
assert span["cache_write_tokens"] == 209000
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def test_cost_rows_carry_cache_tokens(db, client):
|
|
256
|
+
"""`/api/v1/cost` rows + totals include cache-read and cache-write (#17)."""
|
|
257
|
+
sp = make_llm_span(agent_id="a", model="claude-opus-4-8", provider="anthropic",
|
|
258
|
+
input_tokens=2, output_tokens=465, cache_tokens=243597,
|
|
259
|
+
cache_write_tokens=209000, cost_usd=1.4423)
|
|
260
|
+
db.insert_span(sp)
|
|
261
|
+
data = (await client.get("/api/v1/cost?group_by=model")).json()
|
|
262
|
+
assert data["rows"][0]["cache_tokens"] == 243597
|
|
263
|
+
assert data["rows"][0]["cache_write_tokens"] == 209000
|
|
264
|
+
assert data["total_cache_write_tokens"] == 209000
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def test_cost_includes_window_series_for_chart(client):
|
|
268
|
+
"""/api/v1/cost carries a window-bucketed series (per bucket+agent+model)
|
|
269
|
+
plus the bucket size and window bounds so the chart can span the full
|
|
270
|
+
selected window with zero-fill (#113/#133)."""
|
|
271
|
+
await _ingest_sample_span(client)
|
|
272
|
+
resp = await client.get("/api/v1/cost", params={"since": "7d", "group_by": "day"})
|
|
273
|
+
assert resp.status_code == 200
|
|
274
|
+
data = resp.json()
|
|
275
|
+
assert "series" in data and isinstance(data["series"], list)
|
|
276
|
+
assert data["series_bucket"] in ("hour", "day")
|
|
277
|
+
assert isinstance(data["window_start"], int) and isinstance(data["window_end"], int)
|
|
278
|
+
assert data["window_end"] >= data["window_start"]
|
|
279
|
+
if data["series"]:
|
|
280
|
+
item = data["series"][0]
|
|
281
|
+
assert {"bucket", "agent_id", "model", "cost_usd",
|
|
282
|
+
"input_tokens", "output_tokens"} <= set(item)
|
|
283
|
+
assert isinstance(item["bucket"], int) # epoch seconds
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def test_cost_series_buckets_hourly_for_short_window(client):
|
|
287
|
+
"""A ≤2-day window buckets hourly so 24h renders with hourly ticks (#133)."""
|
|
288
|
+
await _ingest_sample_span(client)
|
|
289
|
+
resp = await client.get("/api/v1/cost", params={"since": "24h"})
|
|
290
|
+
assert resp.json()["series_bucket"] == "hour"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
_FRAMING_KEYS = {
|
|
294
|
+
"pricing_mode", "plan_tier", "plan_label", "plan_monthly_usd",
|
|
295
|
+
"subscription_share_pct", "api_share_pct", "display_rule", "qualifier_text",
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def test_cost_response_includes_framing_block(client):
|
|
300
|
+
await _ingest_sample_span(client)
|
|
301
|
+
resp = await client.get("/api/v1/cost")
|
|
302
|
+
assert resp.status_code == 200
|
|
303
|
+
framing = resp.json()["framing"]
|
|
304
|
+
assert _FRAMING_KEYS <= set(framing)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def test_optimize_response_includes_framing_block(client):
|
|
308
|
+
await _ingest_sample_span(client)
|
|
309
|
+
resp = await client.get("/api/v1/optimize?since=30d")
|
|
310
|
+
assert resp.status_code == 200
|
|
311
|
+
data = resp.json()
|
|
312
|
+
if data.get("error") == "no_data":
|
|
313
|
+
pytest.skip("no spans landed for optimize in this fixture")
|
|
314
|
+
assert _FRAMING_KEYS <= set(data["framing"])
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
async def test_optimize_response_always_carries_downgrade_key(client):
|
|
318
|
+
"""The downsize typed slot must always be present (null when no candidates)
|
|
319
|
+
so the UI can always render a Downsize section (#126)."""
|
|
320
|
+
await _ingest_sample_span(client)
|
|
321
|
+
resp = await client.get("/api/v1/optimize?since=30d")
|
|
322
|
+
data = resp.json()
|
|
323
|
+
if data.get("error") == "no_data":
|
|
324
|
+
pytest.skip("no spans landed for optimize in this fixture")
|
|
325
|
+
assert "downgrade" in data # present even when null
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def test_root_serves_lens_title(client):
|
|
329
|
+
"""Brand pass (#114): the served dashboard <title> is 'TokenJam Lens'."""
|
|
330
|
+
resp = await client.get("/")
|
|
331
|
+
assert resp.status_code == 200
|
|
332
|
+
assert "<title>TokenJam Lens</title>" in resp.text
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async def test_optimize_chain_framing_and_recoverable_fields(client):
|
|
336
|
+
"""The framing block (#110) + per-finding recoverable fields (#111) are
|
|
337
|
+
both present on /api/v1/optimize — validates the chain Overview relies on."""
|
|
338
|
+
await _ingest_sample_span(client)
|
|
339
|
+
resp = await client.get("/api/v1/optimize?since=30d")
|
|
340
|
+
data = resp.json()
|
|
341
|
+
if data.get("error") == "no_data":
|
|
342
|
+
pytest.skip("no spans landed for optimize in this fixture")
|
|
343
|
+
assert _FRAMING_KEYS <= set(data["framing"])
|
|
344
|
+
# The savings analyzers carry the recoverable contract fields (#111).
|
|
345
|
+
# cache-recommend / budget-projection intentionally do not.
|
|
346
|
+
findings = data.get("findings") or {}
|
|
347
|
+
for name in ("cache", "script", "trim"):
|
|
348
|
+
if name in findings:
|
|
349
|
+
assert "estimated_recoverable_usd" in findings[name]
|
|
350
|
+
assert "estimate_basis" in findings[name]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def test_optimize_fast_skips_trim(client):
|
|
354
|
+
"""fast=true skips the expensive Trim analyzer and reports it (#114)."""
|
|
355
|
+
await _ingest_sample_span(client)
|
|
356
|
+
resp = await client.get("/api/v1/optimize?since=30d&fast=true")
|
|
357
|
+
assert resp.status_code == 200
|
|
358
|
+
data = resp.json()
|
|
359
|
+
if data.get("error") == "no_data":
|
|
360
|
+
pytest.skip("no spans landed for optimize in this fixture")
|
|
361
|
+
assert "trim" in data.get("skipped_analyzers", [])
|
|
362
|
+
assert "trim" not in (data.get("findings") or {})
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
async def test_budget_framing_reflects_configured_subscription_plan(db):
|
|
366
|
+
"""The budget surface has no window, so framing falls back to the
|
|
367
|
+
declared plan in config (#110)."""
|
|
368
|
+
from tokenjam.core.config import ProviderBudget
|
|
369
|
+
|
|
370
|
+
cfg = TjConfig(
|
|
371
|
+
version="1",
|
|
372
|
+
security=SecurityConfig(ingest_secret=INGEST_SECRET),
|
|
373
|
+
api=ApiConfig(auth=ApiAuthConfig(enabled=False)),
|
|
374
|
+
)
|
|
375
|
+
cfg.budgets["anthropic"] = ProviderBudget(plan="max_5x")
|
|
376
|
+
pipeline = IngestPipeline(db=db, config=cfg)
|
|
377
|
+
app = create_app(config=cfg, db=db, ingest_pipeline=pipeline)
|
|
378
|
+
transport = httpx.ASGITransport(app=app)
|
|
379
|
+
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
|
|
380
|
+
resp = await c.get("/api/v1/budget")
|
|
381
|
+
assert resp.status_code == 200
|
|
382
|
+
framing = resp.json()["framing"]
|
|
383
|
+
assert framing["pricing_mode"] == "subscription"
|
|
384
|
+
assert framing["plan_tier"] == "max_5x"
|
|
385
|
+
assert framing["plan_label"] == "Max 5x plan"
|
|
386
|
+
assert framing["display_rule"] == "suppress_dollars_for_subscription_share"
|
|
387
|
+
|
|
388
|
+
|
|
241
389
|
async def test_get_alerts_returns_list(client):
|
|
242
390
|
resp = await client.get("/api/v1/alerts")
|
|
243
391
|
assert resp.status_code == 200
|