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