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
| Provider | Wrap* function | Underlying client interface |
|---|---|---|
| OpenAI | dexcost.WrapOpenAI(inner) | CreateChatCompletion(ctx, req) (e.g. sashabaranov/go-openai) |
| Anthropic | dexcost.WrapAnthropic(inner) | Anthropic-compatible client |
| Google Gemini | dexcost.WrapGemini(inner) | Google Gemini-compatible client |
| AWS Bedrock | dexcost.WrapBedrock(inner) | AWS Bedrock client |
| Cohere | dexcost.WrapCohere(inner) | Cohere-compatible client |
| Groq | dexcost.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 shutdownService 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.015TrackedTask.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 eventHeuristic 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).
Trace links
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.