dexcost
Go

Instrumentation & Integrations

How dexcost captures LLM costs via wrapper clients, records non-LLM HTTP costs, and integrates with Gin, Echo, net/http, langchaingo, and browser automation.

Automatic LLM capture

Go cannot monkey-patch running code, so dexcost uses explicit wrapper clients. After calling dexcost.Init(), wrap each provider client once and use the wrapper everywhere in your application. The wrapper intercepts every call and records an llm_call event with token counts, computed cost, and latency.

Provider matrix

ProviderWrap* functionUnderlying client interface
OpenAIdexcost.WrapOpenAI(inner)CreateChatCompletion(ctx, req) (e.g. sashabaranov/go-openai)
Anthropicdexcost.WrapAnthropic(inner)Anthropic-compatible client
Google Geminidexcost.WrapGemini(inner)Google Gemini-compatible client
AWS Bedrockdexcost.WrapBedrock(inner)AWS Bedrock client
Coheredexcost.WrapCohere(inner)Cohere-compatible client
Groqdexcost.WrapGroq(inner)Groq-compatible client

Each Wrap* function accepts the inner client as interface{} so that dexcost has no hard import dependency on any provider SDK. The inner client must satisfy the expected call signature for its provider — see clients/tracked_*.go for the exact interfaces each wrapper expects.

import (
    "github.com/DexwoxBusiness/dexcost-go"
    openai "github.com/sashabaranov/go-openai"
)

func main() {
    dexcost.Init(dexcost.Config{APIKey: "dx_live_..."})
    defer dexcost.Close()

    inner := openai.NewClient(os.Getenv("OPENAI_API_KEY"))
    client := dexcost.WrapOpenAI(inner)

    ctx, task := dexcost.StartTask(ctx, "chat_response",
        dexcost.WithCustomer("acme"),
    )
    defer task.End(dexcost.StatusSuccess)

    resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
        Model:    openai.GPT4o,
        Messages: []openai.ChatCompletionMessage{{Role: "user", Content: "Hello"}},
    })
}

Each wrapper panics if dexcost.Init() has not been called first.

RecordLiteLLM and MCP recording helpers

LiteLLM is a Python gateway library with no Go native client to wrap. When your Go service receives LiteLLM-style response payloads, pass them to dexcost.RecordLiteLLM to record an llm_call event against the active task:

// response is a LiteLLM-shaped map[string]interface{} received from your gateway
event, err := dexcost.RecordLiteLLM(ctx, response)

The response map must contain "model" (string) and a "usage" sub-map with "prompt_tokens" and "completion_tokens". An optional "_hidden_params.custom_llm_provider" field overrides the provider prefix used on the recorded event.

MCP tool calls are recorded with clients.RecordMCPResponse, which looks up cost via the rate registry (key "mcp:<tool_name>") or the 163-service MCPToolMap, then falls back to cost_usd=0, confidence="unknown":

import "github.com/DexwoxBusiness/dexcost-go/clients"

event, err := clients.RecordMCPResponse(
    dexcost.Tracker().Buffer(),
    dexcost.Tracker().Rates(),
    task.TaskID,
    clients.MCPCallInfo{
        ToolName:  "tavily_search",
        Server:    "mcp-server-1",
        LatencyMs: 120,
        IsError:   false,
    },
)

Streaming

OpenAI, Anthropic, and Groq streaming responses are handled by StreamRecorder in clients/streaming.go. The recorder wraps the response body (io.ReadCloser), reads SSE chunks transparently, accumulates token usage, and records one llm_call event when the stream closes or reaches EOF.

Use the provider-specific constructors:

import "github.com/DexwoxBusiness/dexcost-go/clients"

recorder := clients.NewOpenAIStreamRecorder(
    resp.Body,
    dexcost.Tracker().Buffer(),
    dexcost.Tracker().Pricing(),
    task.TaskID,
    "gpt-4o", // requestModel fallback
)
// substitute recorder for resp.Body; read normally
defer recorder.Close()
io.Copy(w, recorder)

For Anthropic streaming use clients.NewAnthropicStreamRecorder; for Groq use clients.NewGroqStreamRecorder. All three constructors accept the same parameters. The event is recorded exactly once — on the first Close() or terminal Read() error.


HTTP & non-LLM cost capture

Per-client tracking with TrackHTTP

adapters.TrackHTTP wraps a single *http.Client with a trackingTransport RoundTripper that records external_cost events for matching domains without mutating the original client:

import (
    "net/http"
    "github.com/DexwoxBusiness/dexcost-go/adapters"
)

base := &http.Client{Timeout: 10 * time.Second}
tracked := adapters.TrackHTTP(base)

// Use tracked wherever you would use base; base is unchanged
resp, err := tracked.Do(req.WithContext(ctx))

Process-wide tracking with EnableGlobalHTTPTracking

Setting TrackHTTP: true in dexcost.Config enables process-wide HTTP cost capture on Init. This wraps http.DefaultTransport so all code that uses http.Get, http.DefaultClient, or a *http.Client with a nil Transport is automatically tracked. dexcost.Close() restores the original transport.

You can also enable it explicitly:

import "github.com/DexwoxBusiness/dexcost-go/adapters"

adapters.EnableGlobalHTTPTracking()
// ... application code ...
adapters.DisableGlobalHTTPTracking() // call on shutdown

Service catalog

The bundled service catalog maps 163 external-service domains to pricing rules. When an HTTP request's hostname matches a catalog entry, the transport records an external_cost event automatically — no rate registration needed. Examples include Pinecone, Stripe, Twilio, SendGrid, Firecrawl, and Exa Search.

To replace the bundled catalog with a remote one at startup, set ServiceCatalogURL in dexcost.Config:

dexcost.Init(dexcost.Config{
    APIKey:            "dx_live_...",
    ServiceCatalogURL: "https://catalog.example.com/dexcost-catalog.json",
})

On Init, the SDK fetches the URL and passes the resulting ServiceCatalog to adapters.SetServiceCatalog.

Domain rate registry

For services not in the bundled catalog, register a per-request rate with adapters.RegisterDomainRate. User-registered rates take precedence over catalog entries:

import (
    "github.com/DexwoxBusiness/dexcost-go/adapters"
    "github.com/shopspring/decimal"
)

adapters.RegisterDomainRate(
    "api.example.com",
    decimal.NewFromFloat(0.005),
    "request",
)

After registration, every HTTP request whose Host header matches api.example.com records an external_cost event with the configured cost. adapters.GetDomainRates() returns a snapshot of all registered rates. adapters.ClearDomainRates() removes all registrations.


Manual recording

RecordCost — top-level

dexcost.RecordCost records a non-LLM cost against the task stored in the provided context:

import "github.com/shopspring/decimal"

err := dexcost.RecordCost(
    ctx,
    "pinecone",
    "query",
    decimal.NewFromFloat(0.004),
    dexcost.WithCostConfidence(dexcost.CostConfidenceExact),
)

Returns errNoActiveTask if no task is attached to ctx.

TrackedTask.RecordCost

Records a non-LLM cost directly on a TrackedTask. Accepts the same EventOption values:

err := task.RecordCost(
    "google_maps_api",
    decimal.NewFromFloat(0.005),
    dexcost.WithOperation("geocode"),
)

Returns core.ErrTaskAlreadyEnded if task.End() has already been called.

TrackedTask.RecordUsage

Looks up service in the rate registry, multiplies the registered rate by units, and records an external_cost event. When no rate is registered, the event is recorded with cost_usd=0 and cost_confidence="unknown" so the call still appears in reports:

dexcost.Tracker().Rates().Register(
    "maps.googleapis.com",
    "request",
    decimal.NewFromFloat(0.005),
)

err := task.RecordUsage("maps.googleapis.com", 3) // records 3 × $0.005 = $0.015

TrackedTask.RecordLLMCall

Manually records an llm_call event. When no explicit cost is supplied, the pricing engine auto-computes it from the bundled LiteLLM pricing data:

err := task.RecordLLMCall(
    "openai",
    "gpt-4o",
    800,  // inputTokens
    150,  // outputTokens
    dexcost.WithCachedTokens(200),
    dexcost.WithLatency(420),
    dexcost.WithErrorType("rate_limit"),
)

Pass dexcost.WithCost(decimal.NewFromFloat(0.012)) to supply an explicit cost and bypass auto-pricing. WithCacheCreationTokens sets the Anthropic prompt-cache write token count, which is charged at a separate per-token rate.

Tasks — StartTask and nesting

dexcost.StartTask returns a derived context and a *TrackedTask. Child tasks opened inside a parent context are linked automatically via parent_task_id:

ctx, pipeline := dexcost.StartTask(ctx, "pipeline",
    dexcost.WithCustomer("acme"),
    dexcost.WithProject("chatbot-v2"),
)
defer pipeline.End(dexcost.StatusSuccess)

// Inner task is automatically linked to pipeline
innerCtx, step := dexcost.StartTask(ctx, "step_one")
defer step.End(dexcost.StatusSuccess)

dexcost.EndTask(ctx, status) is a convenience that retrieves the task from context and calls End on it.

Retry tracking

Flag a retry explicitly with task.MarkRetry:

_, err := client.CreateChatCompletion(ctx, req)
if err != nil {
    task.MarkRetry("rate_limit", dexcost.WithRetryCost(decimal.NewFromFloat(0.001)))
}

MarkRetry creates a retry_marker event that contributes to the task's retry_count and retry_cost_usd aggregates. MarkNotRetry reverses a false-positive: pass uuid.Nil to un-flag the most recent retry event, or a specific event UUID to target it:

task.MarkNotRetry(uuid.Nil)   // clear most recent retry event
task.MarkNotRetry(eventID)    // clear a specific event

Heuristic retry detection is opt-in via EnableRetryHeuristics: true in dexcost.Config. When enabled, RecordLLMCall inspects recent events in the same task and automatically sets is_retry=true if a prior call for the same model used WithErrorType with a transient error class ("rate_limit", "timeout", "5xx", "server_error", "connection_error") within the configured sliding window (default 30 seconds, configurable via RetryHeuristicWindow) and above the confidence threshold (default 0.8, configurable via RetryHeuristicThreshold).

task.LinkTrace attaches an external observability trace to the task. Multiple calls accumulate entries in task.Metadata["_trace_links"]:

task.LinkTrace("langfuse", "trace-abc-123")
task.LinkTrace("datadog", "span-xyz-456")

links := task.GetTraceLinks()
// []map[string]string{
//   {"provider": "langfuse", "trace_id": "trace-abc-123"},
//   {"provider": "datadog",  "trace_id": "span-xyz-456"},
// }

Framework integrations

Gin

middleware.GinMiddleware automatically starts a dexcost task for each request and ends it with StatusSuccess for responses below 500 or StatusFailed for 5xx responses and panics:

import (
    "github.com/gin-gonic/gin"
    "github.com/DexwoxBusiness/dexcost-go"
    "github.com/DexwoxBusiness/dexcost-go/middleware"
)

r := gin.Default()
r.Use(middleware.GinMiddleware(dexcost.Tracker(), "api_request"))

Additional core.TaskOption values (e.g. core.WithCustomer(...)) may be passed as variadic arguments and are applied to every task created by the middleware.

Echo

middleware.EchoMiddleware follows the same pattern for Echo v4. The task ends with StatusFailed when the handler returns a non-nil error or the response status is ≥ 500:

import (
    "github.com/labstack/echo/v4"
    "github.com/DexwoxBusiness/dexcost-go/middleware"
)

e := echo.New()
e.Use(middleware.EchoMiddleware(dexcost.Tracker(), "api_request"))

net/http

middleware.HTTPMiddleware returns a standard func(http.Handler) http.Handler wrapper compatible with any net/http mux:

import (
    "net/http"
    "github.com/DexwoxBusiness/dexcost-go/middleware"
)

mux := http.NewServeMux()
mux.Handle("/chat", middleware.HTTPMiddleware(dexcost.Tracker(), "chat_request")(chatHandler))

middleware.HTTPMiddlewareFunc is a convenience variant that wraps an http.HandlerFunc directly.

langchaingo — DexcostCallbackHandler

integrations.DexcostCallbackHandler is a duck-typed callback handler for langchaingo. It satisfies the callbacks.Handler interface through Go's structural typing without importing langchaingo as a dependency:

import (
    "github.com/DexwoxBusiness/dexcost-go"
    "github.com/DexwoxBusiness/dexcost-go/integrations"
    "github.com/tmc/langchaingo/llms/openai"
)

handler := integrations.NewDexcostCallbackHandler(
    dexcost.Tracker().Buffer(),
    dexcost.Tracker().Pricing(),
)

llm, _ := openai.New(openai.WithModel("gpt-4o"),
    openai.WithCallbacksHandler(handler),
)

ctx, task := dexcost.StartTask(ctx, "lc_chain", dexcost.WithCustomer("acme"))
defer task.End(dexcost.StatusSuccess)

content, err := llm.Call(ctx, "Summarise this document")

Call handler.WithModel("gpt-4o") if the model name is not available from the callback output — it returns the handler for chaining and sets a default model used when HandleLLMEnd cannot extract one.

The handler records one llm_call event per HandleLLMEnd invocation. Token counts are extracted from the output via map key lookup (PromptTokens, prompt_tokens, input_tokens, CompletionTokens, completion_tokens, output_tokens). If no task is active in context when HandleLLMEnd fires, the event is silently skipped.

Browser adapter — StartBrowserSession / TrackBrowser

adapters.StartBrowserSession begins timing a browser-automation session. Call End() (typically via defer) to record a compute_cost event proportional to the session's wall-clock duration:

import (
    "github.com/DexwoxBusiness/dexcost-go/adapters"
    "github.com/shopspring/decimal"
)

ctx, task := dexcost.StartTask(ctx, "scrape", dexcost.WithCustomer("acme"))
defer task.End(dexcost.StatusSuccess)

session := adapters.StartBrowserSession(ctx, page.URL(), decimal.Zero)
// decimal.Zero uses adapters.DefaultBrowserRatePerMinute ($0.01/min)
defer session.End()

// ... Playwright automation ...

adapters.TrackBrowser is a callback-style convenience wrapper. It starts a session, calls fn, and always calls End() — even if fn panics or returns an error:

err := adapters.TrackBrowser(ctx, page.URL(), decimal.NewFromFloat(0.01), func() error {
    return page.Goto("https://example.com")
})

The recorded compute_cost event includes wall_clock_seconds, rate_per_minute, and page_url in its Details. The adapter does not import Playwright — it accepts a page URL string, so no hard dependency is introduced.

On this page