tokenjam 0.3.4__tar.gz → 0.3.5__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.3.5}/CLAUDE.md +23 -12
- {tokenjam-0.3.4 → tokenjam-0.3.5}/PKG-INFO +4 -2
- {tokenjam-0.3.4 → tokenjam-0.3.5}/README.md +2 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/installation.md +15 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/pyproject.toml +10 -2
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/package.json +1 -1
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/manual-new-release-tests.md +74 -4
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/manual-pre-release-testing.md +63 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_cost.py +52 -2
- tokenjam-0.3.5/tokenjam/__init__.py +6 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/app.py +3 -0
- tokenjam-0.3.5/tokenjam/api/routes/version.py +20 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_report.py +2 -1
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_tokenmaxx.py +30 -1
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/main.py +5 -1
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/cost.py +15 -5
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/mcp/server.py +14 -1
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/pricing/models.toml +36 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/ui/index.html +10 -1
- tokenjam-0.3.4/tokenjam/__init__.py +0 -1
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/CODEOWNERS +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/pull_request_template.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/workflows/ci.yml +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/workflows/publish-npm.yml +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.github/workflows/publish-pypi.yml +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/.gitignore +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/AGENTS.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/CHANGELOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/CONTRIBUTING.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/LICENSE +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/Makefile +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/SECURITY.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/alerts.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/architecture.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/backfill/helicone.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/backfill/langfuse.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/backfill/otlp.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/backfill/overview.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/claude-code-integration.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/cli-reference.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/configuration.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/export.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/framework-support.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/internal/specs/.gitkeep +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/nemoclaw-integration.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/openclaw.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/optimize/cache.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/optimize/downsize.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/optimize/script.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/optimize/trim.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/policy/overview.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/python-sdk.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/screenshots/tj-alerts.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/screenshots/tj-budget.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/screenshots/tj-cost.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/screenshots/tj-status.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/screenshots/tj-traces.png +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/docs/typescript-sdk.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/alerts_and_drift/_shared.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/alerts_and_drift/drift_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/rag_pipeline.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/research_team.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/router_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/sample_docs/agent_patterns.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/sample_docs/cost_management.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/sample_docs/observability.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/multi/sample_docs/safety.txt +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/openclaw/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_framework/autogen_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_framework/crewai_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_framework/langchain_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_framework/langgraph_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_framework/llamaindex_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_provider/anthropic_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_provider/bedrock_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_provider/gemini_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_provider/litellm_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_provider/openai_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/hallucination-drift/BLOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/hallucination-drift/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/hallucination-drift/scenario.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/retry-loop/BLOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/retry-loop/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/retry-loop/scenario.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/surprise-cost/BLOG.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/surprise-cost/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/incidents/surprise-cost/scenario.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/package-lock.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/client.test.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/client.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/index.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/semconv.test.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/semconv.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/span-builder.test.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/span-builder.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/src/types.ts +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/sdk-ts/tsconfig.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/email_agent_budget_breach.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/email_agent_drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/email_agent_loop.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/email_agent_normal.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/mock_llm.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/agents/test_mock_scenarios.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/conftest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/e2e/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/e2e/conftest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/e2e/test_real_llm.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/factories.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/fixtures/helicone_real_response.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/fixtures/langfuse_real_response.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/fixtures/otlp_sample.json +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/test_api.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/test_cli.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/test_db.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/test_demos.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/test_full_pipeline.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/integration/test_logs_api.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/synthetic/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/synthetic/test_alert_rules.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/synthetic/test_cost_tracking.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/synthetic/test_drift_detection.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/synthetic/test_ingest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/synthetic/test_schema_validation.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/toy_agent/toy_agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_backfill.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_cache_efficacy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_cache_recommend.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_cmd_policy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_cmd_stop.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_cmd_tokenmaxx.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_compare.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_config.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_config_secret_divergence.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_demo_env.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_demo_scenarios.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_export_claude_code.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_formatting.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_ingest_helicone.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_ingest_langfuse.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_ingest_otlp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_litellm_client.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_litellm_integration.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_logs_converter.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_mcp_server.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_models.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_onboard_codex.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_onboard_daemon.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_openclaw_ingest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_optimize.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_pricing_override.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_prompt_bloat.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_spans_stats_repair.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_time_parse.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_transport_401.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_ui_offline.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tests/unit/test_workflow_restructure.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/deps.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/middleware.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/agents.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/budget.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/cost.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/cost_compare.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/logs.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/metrics.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/optimize.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/otlp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/spans.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/status.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/tools.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/api/routes/traces.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_backfill.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_budget.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_cost.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_demo.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_doctor.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_export.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_mcp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_onboard.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_optimize.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_policy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_serve.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_status.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_stop.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_tools.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_traces.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/cli/cmd_uninstall.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/alerts.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/api_backend.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/backfill.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/config.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/db.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/drift.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/export/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/export/claude_code.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/ingest.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/ingest_adapters/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/ingest_adapters/helicone.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/ingest_adapters/otlp.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/models.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/README.md +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/model_downgrade.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/analyzers/workflow_restructure.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/registry.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/runner.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/optimize/types.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/pricing.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/retention.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/core/schema_validator.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/demo/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/demo/env.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/mcp/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/otel/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/otel/exporters.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/otel/otlp_parsing.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/otel/provider.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/otel/semconv.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/py.typed +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/agent.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/bootstrap.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/client.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/http_exporter.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/anthropic.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/autogen.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/base.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/bedrock.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/crewai.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/gemini.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/langchain.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/langgraph.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/litellm.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/llamaindex.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/openai.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/sdk/transport.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/ui/vendor/htm.js +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/ui/vendor/preact-hooks.js +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/ui/vendor/preact.js +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/utils/__init__.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/utils/formatting.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/utils/ids.py +0 -0
- {tokenjam-0.3.4 → tokenjam-0.3.5}/tokenjam/utils/time_parse.py +0 -0
|
@@ -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
|
|
|
@@ -61,10 +61,17 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
61
61
|
- **`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
62
|
- **`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
63
|
- **`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`.
|
|
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 `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
65
|
- **`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
66
|
- **`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
|
|
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`. **Registry strings (the user-facing names) and file names are decoupled**:
|
|
68
|
+
- `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)
|
|
69
|
+
- `budget_projection.py` → `@register("budget-projection")` — per-provider cycle spend vs `[budget.<provider>]` ceiling; only fires when budget > 0
|
|
70
|
+
- `cache_efficacy.py` → `@register("cache")` — current cache-read efficacy per (provider, model)
|
|
71
|
+
- `cache_recommend.py` → `@register("cache-recommend")` — Anthropic-only structural prefix detection for `cache_control` placement
|
|
72
|
+
- `workflow_restructure.py` → `@register("script")` — `(tool_name, arg_shape)` cluster detection for deterministic-script candidates
|
|
73
|
+
- `prompt_bloat.py` → `@register("trim")` — LLMLingua-2 token-significance classification (requires `tokenjam[bloat]` extra)
|
|
74
|
+
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.
|
|
68
75
|
- **`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
76
|
- **`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
77
|
- **`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)`.
|
|
@@ -92,11 +99,12 @@ Post-ingest hooks run synchronously after each span is written to DB:
|
|
|
92
99
|
|
|
93
100
|
- **`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
101
|
- **`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: `
|
|
102
|
+
- **`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.
|
|
103
|
+
- **`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 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
104
|
- **`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
105
|
- **`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
106
|
- **`tj onboard`** (`cmd_onboard.py`) — `--claude-code` and `--codex` flags trigger integration-specific flows. Prompts for plan tier (api / pro / max_5x / max_20x for Anthropic; api / plus / team / enterprise for OpenAI) and writes it to `[budget.<provider>] plan = "..."`. Supports `--reconfigure` to re-prompt against an existing config, and `--plan <tier>` for non-interactive use. 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.
|
|
99
|
-
- **`tj report`** (`cmd_report.py`) — generates standalone HTML visualizations of analyzer findings
|
|
107
|
+
- **`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
108
|
- **`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
109
|
|
|
102
110
|
All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
|
|
@@ -139,11 +147,13 @@ When a span has a `conversation_id` matching an existing session, it's attribute
|
|
|
139
147
|
10. **Use semconv constants** — reference `GenAIAttributes` and `TjAttributes` from `tokenjam/otel/semconv.py` instead of hardcoding OTel attribute name strings.
|
|
140
148
|
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
149
|
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
|
|
150
|
+
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.
|
|
151
|
+
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
152
|
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
|
|
153
|
+
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
154
|
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.
|
|
155
|
+
18. **Web UI must work fully offline** — `tokenjam/ui/index.html` is the served dashboard. It is intentionally a single-file SPA with **zero external HTTP loads at render time**. Preact + hooks + htm are vendored under `tokenjam/ui/vendor/` and wired via an `<script type="importmap">`; 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). If you add a CDN font, script, or stylesheet, that test will fail. Vendor the asset locally instead. See issue #87 + PR #88.
|
|
156
|
+
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.
|
|
147
157
|
|
|
148
158
|
## Config
|
|
149
159
|
|
|
@@ -233,10 +243,11 @@ Key runtime dependency: `pytz` is required by DuckDB for `TIMESTAMPTZ` column ha
|
|
|
233
243
|
- **[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
244
|
- **[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
245
|
- **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 (`
|
|
246
|
+
- [`downsize.md`](docs/optimize/downsize.md) — cheaper-model candidate flagging (registry: `downsize`, file: `model_downgrade.py`)
|
|
247
|
+
- [`cache.md`](docs/optimize/cache.md) — `cache` (current caching ratio) + `cache-recommend` (Anthropic-only breakpoint suggestions)
|
|
248
|
+
- [`script.md`](docs/optimize/script.md) — `script` clustering by `(tool_name, arg_shape)` signature (file: `workflow_restructure.py`)
|
|
249
|
+
- [`trim.md`](docs/optimize/trim.md) — LLMLingua-2 token-significance classifier (`trim`, file: `prompt_bloat.py`), install + capture requirements, performance numbers
|
|
250
|
+
- **[AGENTS.md](AGENTS.md)** — codebase conventions for contributors (referenced from the top-level README).
|
|
240
251
|
- **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
252
|
- **[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
253
|
- **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.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tokenjam"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.5"
|
|
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.
|
|
@@ -13,8 +13,15 @@ Run this after a new release publishes to PyPI to verify it works end-to-end. Th
|
|
|
13
13
|
tj uninstall --yes 2>/dev/null
|
|
14
14
|
rm -rf ~/.tj ~/.config/tj .tj
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Recommended install path (PEP 668-safe on Homebrew Python and
|
|
17
|
+
# Debian 12+/Ubuntu 24+). `--force` so we reinstall even if a prior
|
|
18
|
+
# version is present.
|
|
19
|
+
pipx install --force tokenjam
|
|
17
20
|
tj --version
|
|
21
|
+
|
|
22
|
+
# Older `pip3 install --upgrade tokenjam` path still works inside
|
|
23
|
+
# a clean venv but fails on system Python — that's the bug pipx
|
|
24
|
+
# solves, and verifying pipx is what we ship docs telling users to do.
|
|
18
25
|
```
|
|
19
26
|
|
|
20
27
|
**Pass criteria:** version matches the release being tested.
|
|
@@ -66,6 +73,32 @@ tj optimize --json | python3 -c \
|
|
|
66
73
|
|
|
67
74
|
**Pass criteria:** every positional analyzer name runs without crashing. Optional analyzers (`cache-recommend`, `trim`) surface clear hints when their prereqs aren't met instead of erroring.
|
|
68
75
|
|
|
76
|
+
## 4b. TokenMaxx tier classification
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
tj tokenmaxx
|
|
80
|
+
# [ ] Bordered "TokenJam TokenMaxxing Report" panel renders
|
|
81
|
+
# [ ] On api plan: shows absolute spend; no multiplier line
|
|
82
|
+
# [ ] Action line surfaces either downsize savings or "no obvious
|
|
83
|
+
# savings flagged yet" (both are valid)
|
|
84
|
+
|
|
85
|
+
# Verify the JSON tier label is one of the six valid v0.3.4 tiers.
|
|
86
|
+
tj tokenmaxx --json | python3 -c \
|
|
87
|
+
"import json,sys;d=json.load(sys.stdin);ok={'TokenSipper','TokenModerator','TokenMaxxer','TokenSuperMaxxer','TokenMegaMaxxer','TokenGigaMaxxer'};assert d['tier'] in ok,d['tier'];print('ok:',d['tier'])"
|
|
88
|
+
|
|
89
|
+
# Reconfigure to a subscription plan and re-run — the multiplier line
|
|
90
|
+
# should appear. Pick whichever plan matches your test config.
|
|
91
|
+
tj onboard --claude-code --reconfigure --plan max_5x
|
|
92
|
+
tj tokenmaxx
|
|
93
|
+
# [ ] Multiplier line "That's N× your Max 5x plan cost ($100/mo flat)."
|
|
94
|
+
# [ ] Tier may shift if the multiplier crosses a boundary
|
|
95
|
+
|
|
96
|
+
# Flip back to api so subsequent steps render dollar figures.
|
|
97
|
+
tj onboard --claude-code --reconfigure --plan api
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Pass criteria:** the report renders without crashing, the JSON `tier` field carries one of the 6 v0.3.4 tier names, and the multiplier line appears under a subscription plan.
|
|
101
|
+
|
|
69
102
|
## 5. Backfill adapters (smoke against committed fixtures)
|
|
70
103
|
|
|
71
104
|
```bash
|
|
@@ -123,10 +156,45 @@ Spot-check:
|
|
|
123
156
|
- [ ] Cost page shows non-zero USD values
|
|
124
157
|
- [ ] Sidebar theme toggle works
|
|
125
158
|
|
|
159
|
+
### Offline-UI verification (v0.3.4 — PR #88)
|
|
160
|
+
|
|
161
|
+
Open Chrome DevTools (or your browser's equivalent) → **Network tab** → reload `http://127.0.0.1:7391/`.
|
|
162
|
+
|
|
163
|
+
- [ ] **Zero failed requests** to `fonts.googleapis.com`, `fonts.gstatic.com`, `esm.sh`, or `tokenjam.dev`
|
|
164
|
+
- [ ] Dashboard interactivity works (sidebar nav, tab switches) — proves the vendored Preact / htm under `/ui/vendor/` is being served, not loading from the CDN
|
|
165
|
+
- [ ] Favicon renders (data: URL, no external fetch)
|
|
166
|
+
|
|
167
|
+
Bonus: turn off wifi entirely, hard-refresh, and confirm the page still renders + the JS still hydrates. The whole dashboard must work air-gapped.
|
|
168
|
+
|
|
126
169
|
```bash
|
|
127
170
|
tj stop
|
|
128
171
|
```
|
|
129
172
|
|
|
173
|
+
## 9. Cache cost-correctness (v0.3.4 — PRs #90 + #92)
|
|
174
|
+
|
|
175
|
+
Cache-only spans (cache_read > 0, input/output = 0) used to be costed at $0. Cache-creation tokens on the live OTLP path used to be silently dropped. Both fixed in v0.3.4.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Spans table now has cache_write_tokens (migration 5).
|
|
179
|
+
duckdb ~/.tj/telemetry.duckdb "PRAGMA table_info(spans)" 2>/dev/null \
|
|
180
|
+
| grep cache_write_tokens \
|
|
181
|
+
&& echo "ok: cache_write_tokens column present"
|
|
182
|
+
|
|
183
|
+
# Any captured Anthropic cache-hit span should have non-zero cost_usd.
|
|
184
|
+
duckdb ~/.tj/telemetry.duckdb "
|
|
185
|
+
SELECT COUNT(*) AS hits,
|
|
186
|
+
MIN(cost_usd) AS min_cost
|
|
187
|
+
FROM spans
|
|
188
|
+
WHERE cache_tokens > 0
|
|
189
|
+
AND (input_tokens = 0 OR input_tokens IS NULL)
|
|
190
|
+
AND (output_tokens = 0 OR output_tokens IS NULL)
|
|
191
|
+
" 2>/dev/null
|
|
192
|
+
# [ ] If hits > 0: min_cost > 0 (cache hits ARE being costed; was $0 pre-0.3.4)
|
|
193
|
+
# [ ] If hits = 0: this release's runs didn't trigger a pure cache-only span — fine, unit tests cover the path
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If you don't have `duckdb` CLI installed, skip the SQL checks — the unit + synthetic tests covering these paths run in CI and are the canonical verification.
|
|
197
|
+
|
|
130
198
|
---
|
|
131
199
|
|
|
132
200
|
## Claude Code integration (smoke)
|
|
@@ -166,14 +234,16 @@ tj stop
|
|
|
166
234
|
|
|
167
235
|
| Step | Pass criteria |
|
|
168
236
|
|------|--------------|
|
|
169
|
-
| 1 | `
|
|
237
|
+
| 1 | `pipx install --force tokenjam` succeeds, version matches release |
|
|
170
238
|
| 2 | Onboard prompts for plan tier; config records it; no auto `usd = 200` written |
|
|
171
239
|
| 3 | Example runs without DB-lock errors; CLI shows real USD values |
|
|
172
|
-
| 4 | All
|
|
240
|
+
| 4 | All optimize analyzers run; caveat appears in downgrade JSON; `plan` + `pricing_mode` in JSON output |
|
|
241
|
+
| 4b | `tj tokenmaxx` renders the bordered report panel; JSON `tier` is one of the 6 v0.3.4 tier names; subscription plan shows multiplier line |
|
|
173
242
|
| 5 | All three backfill adapters ingest from fixtures; re-runs are idempotent |
|
|
174
243
|
| 6 | `--compare previous` produces a diff report; `--export-config` writes a snippet with caveat comments |
|
|
175
244
|
| 7 | `tj policy list` renders the unified table |
|
|
176
|
-
| 8 | `tj serve` starts, web UI loads, HTTP fallback works while server holds lock |
|
|
245
|
+
| 8 | `tj serve` starts, web UI loads, HTTP fallback works while server holds lock; **zero external requests in DevTools Network tab** (offline-UI fix shipped in v0.3.4) |
|
|
246
|
+
| 9 | `cache_write_tokens` column present on the spans table (migration 5); cache-hit spans show non-zero cost_usd |
|
|
177
247
|
| Claude Code | Onboard writes settings.json + projects.json; re-run is a no-op |
|
|
178
248
|
| Codex | Onboard writes `[otel]` + `[mcp_servers.tj]` to codex config; secret synced |
|
|
179
249
|
|
|
@@ -176,6 +176,33 @@ tj optimize --json | python3 -c \
|
|
|
176
176
|
"import json,sys;r=json.load(sys.stdin);assert 'plan' in r and 'pricing_mode' in r;print('ok: plan-tier metadata present')"
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
### 6f. TokenMaxx (v0.3.4 — six-tier ladder)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
tj tokenmaxx
|
|
183
|
+
# [ ] Bordered "TokenJam TokenMaxxing Report" panel renders
|
|
184
|
+
# [ ] On api plan: shows absolute spend; no multiplier line
|
|
185
|
+
# [ ] `tj optimize` reference in the action line renders in green bold
|
|
186
|
+
# [ ] Share-line is teal, mentions @tokenjamdev (NOT #tokenmaxx)
|
|
187
|
+
|
|
188
|
+
# Tier label must be one of the v0.3.4 six.
|
|
189
|
+
tj tokenmaxx --json | python3 -c \
|
|
190
|
+
"import json,sys;d=json.load(sys.stdin);ok={'TokenSipper','TokenModerator','TokenMaxxer','TokenSuperMaxxer','TokenMegaMaxxer','TokenGigaMaxxer'};assert d['tier'] in ok,d['tier'];print('ok:',d['tier'])"
|
|
191
|
+
|
|
192
|
+
# Force a different tier by exercising a subscription plan (use whatever
|
|
193
|
+
# plan matches your test data — heavy usage on a Pro plan will land you
|
|
194
|
+
# higher up the ladder than the same usage on a Max-20x plan).
|
|
195
|
+
tj onboard --claude-code --reconfigure --plan max_5x
|
|
196
|
+
tj tokenmaxx
|
|
197
|
+
# [ ] Output now shows "That's N× your Max 5x plan cost ($100/mo flat)."
|
|
198
|
+
# [ ] Tier name follows the v0.3.4 ladder (Sipper / Moderator / Maxxer /
|
|
199
|
+
# SuperMaxxer / MegaMaxxer / GigaMaxxer)
|
|
200
|
+
# [ ] At thresholds: 1× / 4× / 10× / 20× / 50× crosses tier boundaries
|
|
201
|
+
|
|
202
|
+
# Reset to api before continuing
|
|
203
|
+
tj onboard --claude-code --reconfigure --plan api
|
|
204
|
+
```
|
|
205
|
+
|
|
179
206
|
## 7. Plan-tier-aware rendering
|
|
180
207
|
|
|
181
208
|
Reconfigure to a subscription plan and re-run `tj optimize` — output should reframe.
|
|
@@ -317,6 +344,41 @@ Spot-check (don't repeat every theme/typography detail — those are one-time UI
|
|
|
317
344
|
|
|
318
345
|
If any UI element regresses *visibly broken* relative to main, dig in. Otherwise move on.
|
|
319
346
|
|
|
347
|
+
### Offline-UI verification (v0.3.4 — issue #87 / PR #88)
|
|
348
|
+
|
|
349
|
+
The dashboard must work fully offline. The "local-first, no data egress" pitch breaks the moment a render-time external load happens.
|
|
350
|
+
|
|
351
|
+
Open Chrome DevTools (or your browser's equivalent) → **Network tab** → reload `http://127.0.0.1:7391/`.
|
|
352
|
+
|
|
353
|
+
- [ ] **Zero failed requests** to `fonts.googleapis.com`, `fonts.gstatic.com`, `esm.sh`, or `tokenjam.dev`
|
|
354
|
+
- [ ] Dashboard interactivity works (sidebar nav, tab switches) — proves the vendored Preact / htm under `/ui/vendor/` is serving correctly
|
|
355
|
+
- [ ] Favicon renders (data: URL, no external fetch)
|
|
356
|
+
|
|
357
|
+
The `tests/unit/test_ui_offline.py` regression test pins this contract in CI, but a manual eyeball catches anything the regex assertions miss (e.g. background-image URLs in CSS, inline `fetch()` calls in JS).
|
|
358
|
+
|
|
359
|
+
### Cache cost-correctness (v0.3.4 — PRs #90 + #92)
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# Spans table has cache_write_tokens (migration 5).
|
|
363
|
+
duckdb ~/.tj/telemetry.duckdb "PRAGMA table_info(spans)" 2>/dev/null \
|
|
364
|
+
| grep cache_write_tokens \
|
|
365
|
+
&& echo "ok: cache_write_tokens column present"
|
|
366
|
+
|
|
367
|
+
# Any Anthropic cache-hit span (cache_read>0, no input/output) is now
|
|
368
|
+
# costed. Pre-0.3.4 these were dropped as no-ops and silently $0.
|
|
369
|
+
duckdb ~/.tj/telemetry.duckdb "
|
|
370
|
+
SELECT COUNT(*) AS hits, MIN(cost_usd) AS min_cost
|
|
371
|
+
FROM spans
|
|
372
|
+
WHERE cache_tokens > 0
|
|
373
|
+
AND (input_tokens = 0 OR input_tokens IS NULL)
|
|
374
|
+
AND (output_tokens = 0 OR output_tokens IS NULL)
|
|
375
|
+
" 2>/dev/null
|
|
376
|
+
# [ ] If hits > 0: min_cost > 0
|
|
377
|
+
# [ ] If hits = 0: this run didn't trigger a pure cache-only span — fine
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
If `duckdb` CLI isn't installed, skip — the unit + synthetic tests covering these paths run in CI.
|
|
381
|
+
|
|
320
382
|
## 14. Clean up
|
|
321
383
|
|
|
322
384
|
```bash
|
|
@@ -402,6 +464,7 @@ tj status && tj traces && tj cost --since 1h
|
|
|
402
464
|
tj optimize # all analyzers
|
|
403
465
|
tj optimize downsize
|
|
404
466
|
tj optimize cache
|
|
467
|
+
tj tokenmaxx # six-tier ladder
|
|
405
468
|
tj backfill langfuse --source-file tests/fixtures/langfuse_real_response.json
|
|
406
469
|
tj backfill helicone --source-file tests/fixtures/helicone_real_response.json
|
|
407
470
|
tj backfill otlp --source-file tests/fixtures/otlp_sample.json
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
import logging
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
5
7
|
from tokenjam.core.cost import calculate_cost
|
|
6
8
|
from tokenjam.core.pricing import load_pricing_table, get_rates
|
|
7
9
|
|
|
@@ -56,12 +58,60 @@ def test_calculate_cost_cache_read_only():
|
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def test_calculate_cost_unknown_model_uses_default(caplog):
|
|
61
|
+
# Use a unique provider/model so the dedupe set doesn't suppress this run.
|
|
59
62
|
with caplog.at_level(logging.WARNING, logger="tokenjam.core.cost"):
|
|
60
|
-
cost = calculate_cost("
|
|
63
|
+
cost = calculate_cost("test_unknown_provider", "test_unknown_model", 1_000_000, 1_000_000)
|
|
61
64
|
# Default rates: 0.50 input, 2.00 output per MTok
|
|
62
65
|
# (1M/1M * 0.50) + (1M/1M * 2.00) = 2.50
|
|
63
66
|
assert cost == 2.5
|
|
64
|
-
assert "No pricing data for
|
|
67
|
+
assert "No pricing data for test_unknown_provider/test_unknown_model" in caplog.text
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_calculate_cost_unknown_model_warns_only_once_per_pair(caplog):
|
|
71
|
+
"""Backfilling many spans of the same unknown model used to spam the
|
|
72
|
+
warning N times. Now it's emitted once per (provider, model) per
|
|
73
|
+
process. Issue #98."""
|
|
74
|
+
import tokenjam.core.cost as cost_mod
|
|
75
|
+
# Reset dedupe set so this test is isolated.
|
|
76
|
+
cost_mod._UNKNOWN_MODEL_WARNED.clear()
|
|
77
|
+
|
|
78
|
+
with caplog.at_level(logging.WARNING, logger="tokenjam.core.cost"):
|
|
79
|
+
for _ in range(5):
|
|
80
|
+
calculate_cost("test_provider_xyz", "test_model_xyz", 1000, 200)
|
|
81
|
+
|
|
82
|
+
# Exactly one warning, not five.
|
|
83
|
+
matching = [r for r in caplog.records if "test_provider_xyz/test_model_xyz" in r.message]
|
|
84
|
+
assert len(matching) == 1, f"expected 1 warning, got {len(matching)}"
|
|
85
|
+
|
|
86
|
+
# A DIFFERENT unknown model in the same process should still warn (once).
|
|
87
|
+
caplog.clear()
|
|
88
|
+
with caplog.at_level(logging.WARNING, logger="tokenjam.core.cost"):
|
|
89
|
+
for _ in range(3):
|
|
90
|
+
calculate_cost("test_provider_xyz", "different_model", 1000, 200)
|
|
91
|
+
matching = [r for r in caplog.records if "different_model" in r.message]
|
|
92
|
+
assert len(matching) == 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_deprecated_anthropic_base_models_are_priced():
|
|
96
|
+
"""Dated variants (claude-sonnet-4-20250514, etc.) resolve via the
|
|
97
|
+
YYYYMMDD-stripping fallback to the deprecated base entries we added in
|
|
98
|
+
pricing/models.toml. Issue #98 — was previously falling through to
|
|
99
|
+
defaults and spamming warnings."""
|
|
100
|
+
# Sonnet 4 (deprecated): $3 / $15 per MTok
|
|
101
|
+
cost = calculate_cost("anthropic", "claude-sonnet-4-20250514", 1_000_000, 1_000_000)
|
|
102
|
+
assert cost == pytest.approx(18.0) # 3 + 15
|
|
103
|
+
|
|
104
|
+
# Opus 4 (deprecated): $15 / $75 per MTok
|
|
105
|
+
cost = calculate_cost("anthropic", "claude-opus-4-20250514", 1_000_000, 1_000_000)
|
|
106
|
+
assert cost == pytest.approx(90.0) # 15 + 75
|
|
107
|
+
|
|
108
|
+
# Opus 4.1 (deprecated): $15 / $75 per MTok
|
|
109
|
+
cost = calculate_cost("anthropic", "claude-opus-4-1-20250805", 1_000_000, 1_000_000)
|
|
110
|
+
assert cost == pytest.approx(90.0)
|
|
111
|
+
|
|
112
|
+
# Haiku 3.5 (retired): $0.80 / $4 per MTok
|
|
113
|
+
cost = calculate_cost("anthropic", "claude-haiku-3-5-20241022", 1_000_000, 1_000_000)
|
|
114
|
+
assert cost == pytest.approx(4.8) # 0.8 + 4
|
|
65
115
|
|
|
66
116
|
|
|
67
117
|
def test_calculate_cost_zero_tokens_returns_zero_no_warning(caplog):
|
|
@@ -75,6 +75,7 @@ def create_app(
|
|
|
75
75
|
from tokenjam.api.routes.agents import router as agents_router
|
|
76
76
|
from tokenjam.api.routes.optimize import router as optimize_router
|
|
77
77
|
from tokenjam.api.routes.cost_compare import router as cost_compare_router
|
|
78
|
+
from tokenjam.api.routes.version import router as version_router, health_router
|
|
78
79
|
|
|
79
80
|
app.include_router(spans_router, prefix="/api/v1")
|
|
80
81
|
app.include_router(traces_router, prefix="/api/v1")
|
|
@@ -87,6 +88,8 @@ def create_app(
|
|
|
87
88
|
app.include_router(agents_router, prefix="/api/v1")
|
|
88
89
|
app.include_router(optimize_router, prefix="/api/v1")
|
|
89
90
|
app.include_router(cost_compare_router, prefix="/api/v1")
|
|
91
|
+
app.include_router(version_router, prefix="/api/v1")
|
|
92
|
+
app.include_router(health_router) # /health — no prefix, for uptime probes
|
|
90
93
|
app.include_router(metrics_router) # /metrics — no prefix
|
|
91
94
|
app.include_router(otlp_router) # /v1/traces, /v1/metrics, /v1/logs — no prefix
|
|
92
95
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""GET /api/v1/version — package version, used by the UI footer.
|
|
2
|
+
GET /health — process liveness probe (alias for uptime tooling, no prefix)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
from tokenjam import __version__
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
health_router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/version")
|
|
14
|
+
async def get_version() -> dict:
|
|
15
|
+
return {"version": __version__}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@health_router.get("/health")
|
|
19
|
+
async def get_health() -> dict:
|
|
20
|
+
return {"status": "ok", "version": __version__}
|
|
@@ -19,6 +19,7 @@ from datetime import datetime, timezone
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
21
|
import click
|
|
22
|
+
from rich.markup import escape as _rich_escape
|
|
22
23
|
|
|
23
24
|
from tokenjam.utils.formatting import console
|
|
24
25
|
|
|
@@ -82,7 +83,7 @@ def _render_trim_report(
|
|
|
82
83
|
|
|
83
84
|
if not finding.enabled:
|
|
84
85
|
# Show the hint inline rather than producing an empty HTML file.
|
|
85
|
-
console.print(f"[yellow]Trim analyzer not ready:[/yellow]\n{finding.hint}")
|
|
86
|
+
console.print(f"[yellow]Trim analyzer not ready:[/yellow]\n{_rich_escape(finding.hint)}")
|
|
86
87
|
return
|
|
87
88
|
|
|
88
89
|
if not finding.per_prompt:
|
|
@@ -193,12 +193,41 @@ def _fetch(db, config, since_dt, until_dt, since_str) -> tuple[float, float, int
|
|
|
193
193
|
# ───────────────────────────── helpers ────────────────────────────────────
|
|
194
194
|
|
|
195
195
|
def _config_declared_plan(config) -> str | None:
|
|
196
|
-
"""
|
|
196
|
+
"""Return the user's declared subscription plan tier.
|
|
197
|
+
|
|
198
|
+
Checks the active config first; if no `[budget.<provider>].plan` is set
|
|
199
|
+
(common when running from a project dir whose `.tj/config.toml` has no
|
|
200
|
+
`[budget]` section), falls back to peeking at the global config at
|
|
201
|
+
`~/.config/tj/config.toml`. Without this fallback, tokenmaxx silently
|
|
202
|
+
rendered api-pricing framing in subdirectories even when the user had
|
|
203
|
+
set their plan globally via `tj onboard`. Issue #106.
|
|
204
|
+
"""
|
|
197
205
|
budgets = getattr(config, "budgets", None) or {}
|
|
198
206
|
for provider in sorted(budgets.keys()):
|
|
199
207
|
plan = getattr(budgets[provider], "plan", None)
|
|
200
208
|
if plan:
|
|
201
209
|
return str(plan)
|
|
210
|
+
|
|
211
|
+
# Active config has no plan — peek at the global config file directly.
|
|
212
|
+
try:
|
|
213
|
+
import sys
|
|
214
|
+
from pathlib import Path
|
|
215
|
+
if sys.version_info >= (3, 11):
|
|
216
|
+
import tomllib
|
|
217
|
+
else:
|
|
218
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
219
|
+
global_path = Path.home() / ".config" / "tj" / "config.toml"
|
|
220
|
+
if not global_path.exists():
|
|
221
|
+
return None
|
|
222
|
+
with open(global_path, "rb") as f:
|
|
223
|
+
raw = tomllib.load(f)
|
|
224
|
+
budget_block = raw.get("budget") or {}
|
|
225
|
+
for provider in sorted(budget_block.keys()):
|
|
226
|
+
plan = (budget_block[provider] or {}).get("plan")
|
|
227
|
+
if plan:
|
|
228
|
+
return str(plan)
|
|
229
|
+
except (OSError, Exception): # noqa: BLE001
|
|
230
|
+
return None
|
|
202
231
|
return None
|
|
203
232
|
|
|
204
233
|
|
|
@@ -3,7 +3,11 @@ from tokenjam.core.config import load_config
|
|
|
3
3
|
from tokenjam.core.db import open_db
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
@click.group(
|
|
6
|
+
@click.group(
|
|
7
|
+
epilog="Upgrade with: pipx upgrade tokenjam "
|
|
8
|
+
"(then `tj stop && tj serve &` to reload the daemon). "
|
|
9
|
+
"Verify with `tj --version`.",
|
|
10
|
+
)
|
|
7
11
|
@click.version_option(package_name="tokenjam")
|
|
8
12
|
@click.option("--config", "config_path", default=None, envvar="TJ_CONFIG",
|
|
9
13
|
help="Config file path (default: auto-discover)")
|