Skip to main content

dexcost

Agent Unit Economics SDK — track end-to-end business-task costs for AI agents.

dexcost attributes LLM calls, non-LLM service fees, and retry waste to customers, projects, and workflows so you can answer "what does each AI task actually cost?"

Install

pip install dexcost

With every supported provider, framework, and GPU integration:

pip install dexcost[all]

Or install only the provider integration you use:

pip install "dexcost[openai]"
pip install "dexcost[anthropic]"
pip install "dexcost[litellm]"
pip install "dexcost[gemini]"
pip install "dexcost[bedrock]"
pip install "dexcost[cohere]"
pip install "dexcost[mcp]"
pip install "dexcost[ollama]"
pip install "dexcost[openrouter]"
pip install "dexcost[perplexity]"
pip install "dexcost[fal]"
pip install "dexcost[gpu]"  # NVIDIA NVML task-level GPU accounting

The Gemini extra currently supports google-genai 1.x and 2.x and is bounded to <3.0.0, matching Google's announced next-major breaking-change boundary.

Quick Start

Global API (recommended)

import dexcost

dexcost.init(api_key="dx_live_...")  # or set DEXCOST_API_KEY env var
dexcost.set_context(customer_id="acme-corp")

with dexcost.task(task_type="summarise_doc") as t:
    # LLM calls are auto-captured — just use OpenAI/Anthropic/etc normally
    response = openai.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Summarise this document"}],
    )

    # Record non-LLM costs manually
    t.record_cost(service="pdf_parser", cost_usd="0.002")

    # Record an actual business result explicitly; task success alone is not
    # treated as an achieved outcome.
    t.record_outcome("document_delivered", value=True)

dexcost.close()

Instance API (for multi-tracker scenarios)

from dexcost import CostTracker
from dexcost.storage.sqlite import SQLiteStorage

tracker = CostTracker(storage=SQLiteStorage("/tmp/demo.db"))

with tracker.task(task_type="summarise_doc", customer_id="acme") as t:
    t.record_llm_call("openai", "gpt-4o", input_tokens=800, output_tokens=150)
    t.record_cost(service="pdf_parser", cost_usd="0.002")

CrewAI and Griptape

Install only the framework integration you use:

pip install "dexcost[crewai]"
pip install "dexcost[griptape]"

The wrappers preserve the original framework object and public method signatures. They create one canonical DexCost task when no task is active, or reuse the caller's active task. Sync, async, streaming, failure, cancellation, early-close, and framework-native tool events share that same lifecycle.

import dexcost

dexcost.init(api_key="dx_live_...")

# Existing CrewAI Crew, Agent, LiteAgent, and Flow objects are supported.
tracked_crew = dexcost.track_crewai(crew)
crew_output = tracked_crew.kickoff(inputs={"topic": "unit economics"})

# Existing Griptape Structure objects are supported without replacing drivers.
tracked_agent = dexcost.track_griptape(agent)
tracked_agent.run("Build the report")

For an instance tracker, pass it as the second argument:

from dexcost.integrations import track_crewai, track_griptape

tracked_crew = track_crewai(crew, tracker)
tracked_structure = track_griptape(structure, tracker)

Provider instrumentation is the authoritative LLM-cost path and prevents double counting. Framework event fallback is opt-in for custom providers that DexCost cannot instrument:

tracked_crew = dexcost.track_crewai(crew, capture_llm_events=True)

Do not enable that fallback for the same calls already captured by OpenAI, Anthropic, LiteLLM, Gemini, Bedrock, or another provider instrument. Native tool capture stores only bounded tool identity, opaque framework IDs, status, cache/attempt dimensions, and exact duration when the framework exposes it. Prompts, tool arguments, chain-of-thought, outputs, and error messages are not stored. See FRAMEWORK-COMPATIBILITY.md for the current execution surface and compatibility evidence.

Business outcomes

Outcomes are durable, revisioned business facts linked to a task. DexCost does not infer them from a successful task because completing technical work is not the same as achieving a customer result.

from decimal import Decimal

with dexcost.task(task_type="campaign_export") as task:
    # ...generate and export the campaign...
    exported = task.record_outcome("campaign_exported", value=True)
    task.record_outcome("approved_creative_count", value=2)
    task.record_outcome("quality_score", value=Decimal("0.92"))

# Correct a previously recorded outcome by preserving its identity and
# incrementing the revision. The local ledger rejects gaps and invalid state
# transitions before they reach the control plane.
dexcost.record_outcome(
    "campaign_exported",
    task_id=task.task_id,
    outcome_id=exported.outcome_id,
    revision=2,
    state="missed",
    value=False,
)

Values are exact typed facts: Python strings, booleans, integers, and Decimal values map to the corresponding wire types. Use canonical lowercase names such as campaign_exported; do not place secrets or personal data in an outcome name or value.

Agent and workflow identity

Keep the business workflow, deployed agent, and technical task type as separate dimensions. Set the stable identity once; nested tasks inherit it automatically.

dexcost.set_context(
    customer_id="dexcost-internal",
    project_id="dexcost-marketing-campaign",
    agent="campaign_director",
    agent_version="demo-v1",
    workflow_id="campaign_generation",
    workflow_session_id="campaign-2026-08-17",
)

with dexcost.task(task_type="campaign.run") as campaign:
    with dexcost.task(task_type="campaign.script.generate"):
        generate_script()

    with dexcost.task(task_type="campaign.narration.generate"):
        generate_narration()

    campaign.record_outcome("campaign_exported", value=True)

This produces one canonical campaign hierarchy while preserving the actual work-step task types. Agent identity never replaces task_type, and technical success is not presented as a business outcome unless the application records one explicitly.

Attribution observation fields (contract v3)

Observations are emitted on the schema_version: "3" wire contract. The following optional fields were added to v3 in place — old payloads that omit them stay valid, and the bundled JSON schema (dexcost/attribution/attribution-v3-schema.json) is validated strictly on every emit.

Field Type Rule Source
environment str ^[a-z0-9][a-z0-9._-]{0,63}$ (max 64) init(environment=...) / DEXCOST_ENV
operation.latency_ms int 086400000 whole milliseconds Measured call latency
operation.error object {type, code?}; rejected when operation.status == "succeeded" Instrument failure marker
operation.error.type str ^[a-z0-9][a-z0-9._-]{0,127}$ — canonical taxonomy (timeout, rate_limit) Instrument failure marker
operation.error.code str 1–64 chars, opaque provider code Provider response
resource.type enum model, sku, instance, endpoint, session, tool, other "tool" covers MCP/agent tool calls
assignment.user_id str 1–512 chars, opaque set_context(user_id=...)
assignment.product_id str 1–512 chars, opaque set_context(product_id=...)
dexcost.init(environment="production")  # emitted as observation.environment

dexcost.set_context(
    customer_id="acme-corp",
    project_id="proj-alpha",
    user_id="user-42",              # the end user the work is performed for
    product_id="support-console",   # the product surface driving the work
)

A failed tool call therefore reaches the control plane as:

{
  "schema_version": "3",
  "environment": "production",
  "component": "external",
  "resource": { "type": "tool", "id": "web_browser" },
  "operation": {
    "status": "failed",
    "latency_ms": 1250,
    "error": { "type": "timeout", "code": "ETIMEDOUT" }
  },
  "usage_snapshot": "full",
  "usage": []
}

A succeeded operation must not carry operation.error; the SDK rejects that observation locally instead of shipping it.

Auto-Instrumentation

dexcost auto-instruments 10 AI provider SDKs, the MCP tool client, and 5 HTTP libraries.

LLM Providers

Provider Package Auto-Patched Method
OpenAI / Azure / compatible gateways openai Chat/legacy Completions, structured parse, Responses, embeddings, image generation/edit/variation, audio transcription/translation/speech, Azure host/deployment routing, and OpenRouter/Perplexity-compatible routing (sync + async, including native streams)
Anthropic anthropic Current messages.create sync/async, native streams, cache buckets, and tool calls
LiteLLM litellm Language/Responses, embeddings, image generation/edit/variation, transcription/speech, rerank, moderation, search, OCR, and background Responses/video/batch/fine-tuning jobs (sync + async where exposed, including terminal stream and job reconciliation)
Google Gemini / Enterprise google-genai Models and AsyncModels: content generation/streaming, embeddings, image generation/upscale/edit/recontext/segmentation; foreground Interactions sync/async/SSE
AWS Bedrock boto3/botocore; official Smithy runtime on Python 3.12+ Current Converse/streams; InvokeModel chat, embeddings, images, and rerank; guardrails, CountTokens, durable async media jobs, and Nova Sonic bidirectional speech; exact regional/profile identity, cache TTL buckets, tools, routing, service-tier/latency dimensions, and private ARN hashing
Cohere cohere V1/V2 chat, chat_stream, embed, and rerank (sync + async)
Ollama ollama Module singleton, Client, and AsyncClient chat/generate streams, current and legacy embeddings, web search, and web fetch
OpenRouter openrouter Chat, Responses, embeddings, images, STT, TTS, rerank, video jobs, and generation-cost reconciliation (sync + async and native streams)
Perplexity perplexityai Agent Responses and background jobs, Sonar chat, Search, embeddings/contextualized embeddings, and native streams (sync + async)
fal.ai fal-client Module/client run, subscribe, stream, and durable queue submit/status/result/cancel (sync + async)

Every supported AI call inside a tracked task is captured automatically. LiteLLM also preserves canonical routed-provider identity for OpenAI, Anthropic, Google/Vertex, Azure/Azure AI, Bedrock, Cohere, Hugging Face, Together, Ollama, Mistral, Groq, OpenRouter, Perplexity, fal.ai, xAI, DeepSeek, Fireworks AI, Nvidia NIM, nano-gpt, and other explicit LiteLLM provider/model routes. OpenAI and Google multimodal paths preserve native text/image/audio/video/cache, reasoning, tool-input, character, and media-count quantities, calculate against the active catalog, and retain only quantities and opaque provider IDs. Prompts, media, transcripts, tool payloads, and generated output are not stored. Google stream success is recorded only on natural completion; early close is cancelled and stream exceptions are failed.

HTTP Libraries (Non-LLM Cost Capture)

Library What's Patched
requests Session.send
httpx Client.send
aiohttp ClientSession._request
botocore (boto3) URLLib3Session.send
urllib3 HTTPConnectionPool.urlopen

HTTP calls matching the active service catalog (Pinecone, Twilio, SendGrid, Stripe, Firecrawl, Exa, etc.) are automatically captured as external_cost events with cost extracted from the response. Catalog releases are distributed by the control plane as immutable, content-addressed artifacts and evaluated locally. The SDK validates and caches a last-known-good release for offline use; the bundled catalog remains an emergency bootstrap until Python and TypeScript joint migration gates allow it to be reduced safely.

For signed authority, configure rotated Ed25519 public keys and require a signature. Keys are raw 32-byte public keys encoded as unpadded base64url. Supplying keys requires signatures by default; setting the boolean explicitly is shown for clarity:

dexcost.init(
    api_key="dx_live_...",
    catalog_trusted_keys={"dexcost-prod-2026-01": "<public-key-base64url>"},
    catalog_require_signature=True,
)

Air-gapped hosts use the same validation and activation path:

dexcost.init(
    storage="local",
    catalog_trusted_keys={"dexcost-prod-2026-01": "<public-key-base64url>"},
    catalog_require_signature=True,
)
dexcost.import_catalog_bundle("dexcost-catalog-release.dcr.json")

Import never bypasses signature, expiry, downgrade, size, hash, schema, or semantic checks. The previous release remains available if import fails. When the packaged production trust document has no keys, remote catalog refresh and bundle activation stay disabled and bundled pricing remains active. This state is exposed by dexcost.catalog_status().signature_verification as "disabled_no_trust"; an empty trust document is never treated as permission to accept unsigned catalogs. catalog_require_signature=False is an explicit, temporary migration override and enables unsigned refresh deliberately.

Controlling Instrumentation

# Instrument only specific providers
dexcost.init(auto_instrument=["openai", "gemini"])

# Disable all auto-instrumentation
dexcost.init(auto_instrument=[])

# Disable HTTP tracking
dexcost.init(track_http=False)

Configuration

dexcost.init() Parameters

Parameter Type Default Description
api_key str DEXCOST_API_KEY env API key for cloud push
auto_instrument list[str] All supported instruments Which provider/tool SDKs to patch
track_http bool True Patch HTTP libraries for non-LLM cost capture
batch_size int 100 Events per sync batch
flush_interval float 5.0 Seconds between sync pushes
catalog_trusted_keys Mapping[str, str | bytes] env or packaged production trust Rotated Ed25519 public keys by manifest key ID
catalog_require_signature bool | None True with keys; remote refresh disabled without keys Reject unsigned network releases and durable cache entries; False is an explicit migration override
catalog_refresh_interval float 24 hours Background release refresh interval in seconds
catalog_refresh_jitter float 0.1 Random refresh spread from 0 through 0.5
redact_fields list[str] None Field names to redact from event details
hash_customer_id bool False SHA-256 hash customer_id before storage
environment str None Deployment environment ("production", "staging", …), emitted as observation.environment. "development" also enables dev console mode
storage str None Storage mode ("local" or auto-detect)
endpoint str https://api.dexcost.io Control Layer URL. Must start with http:// or https://. The only way to override the endpoint — it is not read from the environment.
buffer_path str ~/.dexcost/buffer.db Path to local SQLite buffer

Environment Variables

Variable Description
DEXCOST_API_KEY API key (if not passed to init())
DEXCOST_ENV Deployment environment emitted on every observation. Set to development for dev console output
DEXCOST_CATALOG_TRUSTED_KEYS Strict JSON object mapping 1–8 key IDs to unpadded base64url Ed25519 public keys; ignored when catalog_trusted_keys is passed
DEXCOST_CATALOG_REQUIRE_SIGNATURE Strict true or false; used only when catalog_require_signature is omitted. Defaults to true with trusted keys; without keys remote refresh remains disabled unless this is explicitly false

Catalog trust resolves from an explicit option, then the environment, then the public trust document shipped in the package. Private signing keys are never part of an SDK or SDK environment.

Note: DEXCOST_ENDPOINT is no longer read. The Control Layer URL is configured only via init(endpoint="https://...") (default https://api.dexcost.io). This prevents an attacker who controls the process environment from redirecting telemetry and the Bearer API key to a hostile collector.

Task Tracking

Context Manager

with dexcost.task(task_type="resolve_ticket") as t:
    # All LLM/HTTP calls inside are automatically captured
    pass

Decorator

@tracker.track_task(task_type="generate_report", customer_id="acme")
def generate_report(data):
    # LLM calls here are tracked
    pass

Manual Start/End

t = tracker.start_task(task_type="batch_job", customer_id="acme")
# ... do work ...
t.end(status="success")

Cross-process campaign or workflow hierarchy

Use stable UUIDs when a workflow spans workers or short-lived tool processes. Supplying root_task_id opts the task into the revisioned business-identity contract. Create the root once, then pass both root and parent IDs to children:

import uuid

campaign_root = uuid.uuid5(uuid.NAMESPACE_URL, "campaign:dexcost-launch")

with dexcost.task(
    task_type="campaign.run",
    task_id=campaign_root,
    root_task_id=campaign_root,
    experiment_id="creative-angle",
):
    pass

# This may execute in a different process.
with dexcost.task(
    task_type="campaign.scene.render",
    root_task_id=campaign_root,
    parent_task_id=campaign_root,
    experiment_id="creative-angle",
    variant="proof-first",
):
    render_scene()

The identity snapshot is published with the final task update. Task type and assignment fields are immutable for that SDK task identity. The Python SDK currently emits revision 1 only; later corrections belong in the workspace business-attribution API rather than rewriting provider usage.

Local NVIDIA GPU usage

Opt in only on the leaf task that owns the GPU work:

with tracker.task(task_type="local_whisper", track_gpu=True):
    transcribe_on_local_gpu()

DexCost records measured GPU-seconds, the normalized device model, and utilization evidence. Without an explicit user-owned rate, a local GPU and local network transfer remain unpriced; the SDK does not apply public-cloud fallbacks to locally owned infrastructure.

To attribute your own amortized hardware, electricity, hosting, or bandwidth rate, load a versioned YAML file during initialization. The values below are illustrative user inputs, not DexCost public-list prices:

version: 2
rates: {}
infrastructure:
  gpu:
    nvidia-geforce-rtx-5060-ti:
      per: gpu_hour
      cost_usd: "0.25"
  network:
    local:
      per: gb_transferred
      cost_usd: "0.02"
dexcost.init(
    api_key="dx_live_...",
    rates_path="rates.yaml",
)

GPU units can be gpu_second or gpu_hour. Network units can be gb_transferred (request plus response bytes) or gb_egress. Keys are normalized and matched exactly; there is no default-rate fallback. Configured costs carry sdk_rate_registry evidence and a deterministic pricing version. Do not enable GPU measurement on both a parent and its child, because they would measure the same hardware interval twice.

TrackedTask Methods

with dexcost.task(task_type="...") as t:
    # Record LLM call manually (usually auto-captured)
    t.record_llm_call("openai", "gpt-4o", input_tokens=800, output_tokens=150)

    # Record non-LLM cost
    t.record_cost(service="pinecone", cost_usd="0.001")

    # Record usage (cost computed from registered rates)
    t.record_usage(service="s3_storage", units=1024)

    # Mark a retry
    t.mark_retry(reason="rate_limit", cost_usd="0.005")

    # Link to external trace
    t.link_trace(provider="datadog", trace_id="abc123")

Customer Attribution

dexcost.set_context(
    customer_id="acme-corp",
    project_id="proj-alpha",
    user_id="user-42",
    product_id="support-console",
)

# All tasks created after this inherit customer_id, project_id, user_id and
# product_id; they are shipped as the business identity `assignment` snapshot.
with dexcost.task(task_type="...") as t:
    pass  # t.task.customer_id == "acme-corp"

Dev Mode

Set DEXCOST_ENV=development or pass environment="development" to init(). In dev mode:

  • Cost events are printed to the terminal
  • No data is pushed to the cloud
  • Useful for local development and debugging

CLI

dexcost status          # DB location, event count, sync status
dexcost rates --list    # Show registered cost rates
dexcost scan .          # Find untracked cost points in your code
dexcost scan . --generate-stubs  # Generate record_cost() stubs for manual points

Development

pip install -e ".[all]"
pip install ruff black mypy pytest

make lint        # ruff
make format      # black
make typecheck   # mypy strict
make test        # pytest

Releases

Releases are generated from Conventional Commit pull-request titles and are squash-merged to main. Use feat(python): ... for features and fix(python): ... for fixes. See CONTRIBUTING.md and CHANGELOG.md.

Privacy

When you connect to the Dexcost Control Layer, the SDK transmits usage data subject to our Privacy Policy.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dexcost-0.20.0.tar.gz (767.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dexcost-0.20.0-py3-none-any.whl (498.7 kB view details)

Uploaded Python 3

File details

Details for the file dexcost-0.20.0.tar.gz.

File metadata

  • Download URL: dexcost-0.20.0.tar.gz
  • Upload date:
  • Size: 767.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for dexcost-0.20.0.tar.gz
Algorithm Hash digest
SHA256 cc8298184a1c01fb01edf06f10b0358f503839e4929cd54e4cdceadd646558d8
MD5 fb9f070ae7031d79d3e03646714d1e04
BLAKE2b-256 c7ee40a32a6f1d1a22e89942595201c9994304ec66fb05445b58f2ea25a55b42

See more details on using hashes here.

Provenance

The following attestation bundles were made for dexcost-0.20.0.tar.gz:

Publisher: release-please.yml on DexwoxBusiness/dexcost-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dexcost-0.20.0-py3-none-any.whl.

File metadata

  • Download URL: dexcost-0.20.0-py3-none-any.whl
  • Upload date:
  • Size: 498.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for dexcost-0.20.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eab18077d43ba5df7619c34b6ce7391f1f0959179f09b41809e843838d241dad
MD5 0070d73aa3f26ca94aaa796fec431183
BLAKE2b-256 6f31a1e68cf67009fbd846737f1fa335d5a277158eac61c372bdbb5eb9d0f914

See more details on using hashes here.

Provenance

The following attestation bundles were made for dexcost-0.20.0-py3-none-any.whl:

Publisher: release-please.yml on DexwoxBusiness/dexcost-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.20.0 This release

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.2

2 files

0.15.1

2 files

0.15.0

2 files

0.14.2

2 files

0.14.1

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.1

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page