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