Summary: The @pdbr/opencode-plugin-langfuse plugin connects OpenCode to Langfuse via OpenTelemetry. It captures sessions, messages, and costs automatically. Crucially, it bridges the gap between OpenCode’s local cost calculation and OpenTelemetry’s exported spans for accurate, custom-model cost tracking.
🧬 Hyper-Dense Patterns
- In-flight OTel Attribute Mutation (Cost Hydration): Instead of using Langfuse’s public ingestion API (
generation-update)—which triggers race conditions against OTel batched exports—the plugin implements a FIFO buffering pattern. It intercepts AI SDK inner generation spans (ai.*.doGenerateorai.*.doStream), buffers them persessionId, and waits for OpenCode’smessage.updatedevent. It then mutates thelangfuse.observation.cost_detailsattribute directly on the underlyingSpanobject before forwarding to the innerLangfuseSpanProcessor. - Dynamic Per-Component Cost Split: Instead of a flat total, the plugin lazily fetches
client.config.providers(), calculates token deltas per message iteration, applies per-million token rates (forinput,output,cache_read,cache_write, andreasoning), and attaches granular{"input": X, "output": Y, "total": Z}JSON payloads. - Trace Stitching (Parent-Aware Context Management): To nest OpenCode traces into a larger external DAG (e.g., calling OpenCode as a sub-agent), the plugin overrides OTel’s default context propagation:
ParentAwareIdGenerator: Replaces theRandomIdGeneratorto statically return a pre-definedLANGFUSE_TRACE_ID.ParentAwareContextManager: OverridesAsyncLocalStorageContextManager.active()to return a remoteLANGFUSE_PARENT_OBSERVATION_IDcontext when the local context is empty. Root spans become children of the external trace.
- Graceful Shutdown via Bounded Flush: Avoids Node/Bun OTLP keep-alive socket hangs during OpenCode shutdown (
server.instance.disposed) by skippingsdk.shutdown()entirely. Uses aPromise.racearoundprocessor.forceFlush()bounded by a configurable timeout (LANGFUSE_DISPOSE_FLUSH_MS, default 8000ms).
đźš« Anti-Patterns
- API Cost Writes (
generation-updateAPI): Writing costs via the Langfuse REST API while OpenTelemetry streams the trace creates race conditions. The API write can create a “stub” observation, locking the span ID, causing the delayed OTel span to fail when attempting to update core attributes. - Unbounded Span Queues: Waiting indefinitely for
message.updatedevents to assign cost. If a model generates text but errors out before cost is computed, the span could be buffered forever. The plugin prevents this by evicting the oldest span uncosted if the FIFO queue exceedsMAX_PENDING_PER_SESSION = 64. - Calling
sdk.shutdown()in Bun: Due to Bun’s lingering OTLP keep-alive sockets (oven-sh/bun#13184), calling.shutdown()on the NodeSDK can hang the process permanently. Always rely on a forced flush. - Mutating Outer AI SDK Spans: Attaching cost attributes to
ai.streamTextorai.generateText. These are evaluated asSPANobservations in Langfuse and lack model associations. Costs MUST be attached to the inner generation spans (ai.*.doStream/ai.*.doGenerate), which map toGENERATIONobservations. - Relying Solely on Langfuse Cloud Pricing: Relying on Langfuse to automatically cost tokens will result in
$0costs for custom proxies, local models, or OpenCode-specific local rate overrides.
🏆 Best Practices
- Enable OTel at the Core: You MUST manually opt-in OpenCode to OpenTelemetry in
.opencode/opencode.jsoncby setting"experimental": { "openTelemetry": true }. - Set a Hydration Timeout: Provide a
LANGFUSE_COST_HYDRATION_TIMEOUT_MS(default 10s). If the cost calculation event is dropped by OpenCode, the buffered span will gracefully flush without cost, ensuring token telemetry isn’t lost. - State Delta Tracking: When tracking LLM streaming costs locally, always track the last seen
costandtokensstate (e.g.,info.cost - prev.cost). Since OpenCode streams continuously, treating every update as an absolute total will double-charge observations. - Strictly Serial Sessions: OpenCode sub-agents must execute in their own sessions or use strict serial execution. Shared
sessionIds in parallel requests will corrupt the FIFO span-to-cost attribution logic.