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.*.doGenerate or ai.*.doStream), buffers them per sessionId, and waits for OpenCode’s message.updated event. It then mutates the langfuse.observation.cost_details attribute directly on the underlying Span object before forwarding to the inner LangfuseSpanProcessor.
  • 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 (for input, output, cache_read, cache_write, and reasoning), 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 the RandomIdGenerator to statically return a pre-defined LANGFUSE_TRACE_ID.
    • ParentAwareContextManager: Overrides AsyncLocalStorageContextManager.active() to return a remote LANGFUSE_PARENT_OBSERVATION_ID context 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 skipping sdk.shutdown() entirely. Uses a Promise.race around processor.forceFlush() bounded by a configurable timeout (LANGFUSE_DISPOSE_FLUSH_MS, default 8000ms).

đźš« Anti-Patterns

  • API Cost Writes (generation-update API): 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.updated events 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 exceeds MAX_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.streamText or ai.generateText. These are evaluated as SPAN observations in Langfuse and lack model associations. Costs MUST be attached to the inner generation spans (ai.*.doStream / ai.*.doGenerate), which map to GENERATION observations.
  • Relying Solely on Langfuse Cloud Pricing: Relying on Langfuse to automatically cost tokens will result in $0 costs 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.jsonc by 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 cost and tokens state (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.