Langfuse v4: up to 165× faster · Read more

Migrate from Helicone to Langfuse

This guide walks through migrating LLM observability and prompt management from Helicone to Langfuse: tracing first, then user/session attribution, prompts, and your historical request data.

TL;DR: Helicone captures LLM calls at the gateway; Langfuse captures them in your application via SDK wrappers or OpenTelemetry, outside the request path. The tracing swap is usually a same-day change: replace the Helicone base URL with a traced client (or keep the gateway during a transition window, since the two capture mechanisms don't conflict). Helicone's Helicone-User-Id, session, and Helicone-Property-* headers map to first-class Langfuse users, sessions, and metadata/tags. Prompts export through Helicone's REST API and import as versioned Langfuse prompts with environment labels. Historical requests export via Helicone's @helicone/export CLI and can be selectively reingested over OTLP. Since Helicone is in maintenance mode, export your data early even if you only reingest part of it.

Why teams migrate

Helicone moved into maintenance mode following its acquisition by Mintlify (announced March 2026): services remain live for the foreseeable future, but the product is no longer actively developed. That makes the two usual migration motivations concrete:

  • A maintained, actively developed platform. Langfuse ships tracing, prompt management, evaluation, datasets, and custom dashboards as one actively developed product, open source and fully self-hostable (UI, API, and data in your own infrastructure).
  • Observability moves out of the request path. Helicone's primary integration is a gateway/proxy that sits between your application and the LLM provider. Langfuse SDKs capture traces asynchronously in your application and export them in the background, so the observability layer adds no hop and no availability dependency to your LLM calls. You also get hierarchical traces with nested spans for retrieval steps, tool calls, and agent loops, rather than one log entry per LLM request.

There is a real timing angle: your Helicone request history, prompts, and configuration are only exportable while the service stays up. Even teams that migrate gradually should run the data export (Step 4) early.

Concept mapping

HeliconeLangfuseNotes
Request log entryTrace with nested observationsOne request becomes a generation inside a trace
Helicone-User-Id headeruser_id on the trace (user tracking)Per-user cost and usage views carry over
Helicone-Session-Id / -Name headerssession_id on the trace (sessions)Groups multi-step interactions
Helicone-Session-Path hierarchySpan nesting via instrumentationPath strings become a real trace tree
Helicone-Property-* custom propertiesMetadata and tagsFilterable in tables and dashboards
Prompts + environments (production, staging)Prompts + labelsThe production environment becomes the production label
Prompt variables {{hc:name:type}}Variables {{name}}Type validation moves into application code
AI Gateway (routing, failover)Not needed for tracing; LiteLLM Proxy if you want a gatewayLangfuse SDKs capture client-side
Requests table, dashboardsObservations table, custom dashboardsRebuild saved views as dashboards

Supported data types

DataMove?Path
Live LLM callsYesSDK wrapper, OTel exporter, or LiteLLM Proxy (Step 1)
Users, sessions, custom propertiesYesHeader values re-mapped via propagate_attributes (Step 2)
Prompts (all versions + environments)YesREST API export → Langfuse prompt management with labels (Step 3)
Prompt partials ({{hcp:...}})RecreateComposability or message placeholders
Historical requestsSelective@helicone/export → OTLP reingest for traces you actively need (Step 4)
Gateway features (routing, failover, cache)NoCall providers directly, or use LiteLLM Proxy
Dashboards, saved views, alertsRecreateCustom dashboards and alerts

Step 1: Switch tracing

This is usually a same-day change, and it's reversible: the Langfuse SDK wraps your LLM client inside your application, so you can keep routing through Helicone's gateway during a transition window. Each system captures the call once, on different sides of the request.

Langfuse provides Python and JS/TS SDKs plus native integrations with 100+ frameworks and providers (OpenAI, Anthropic, LangChain, Vercel AI SDK, and more). For OpenAI-compatible clients, which is what Helicone gateway users have, the drop-in wrapper is the fastest path:

# pip install langfuse
# env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
from langfuse.openai import openai  # traced drop-in replacement

client = openai.OpenAI(
    api_key="your-provider-api-key",
    base_url="https://api.openai.com/v1",  # or keep Helicone's gateway URL during transition
)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
)

from langfuse import get_client
get_client().flush()  # flush pending spans before a short-lived script exits
// npm install @langfuse/openai @langfuse/otel @opentelemetry/sdk-node openai
// env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";
import OpenAI from "openai";
import { observeOpenAI } from "@langfuse/openai";

const sdk = new NodeSDK({ spanProcessors: [new LangfuseSpanProcessor()] });
sdk.start();

const client = observeOpenAI(
  new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
    baseURL: "https://api.openai.com/v1", // or keep Helicone's gateway URL during transition
  }),
);

const response = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello!" }],
});

await sdk.shutdown(); // flush pending spans before the script exits

Beyond single LLM calls, the @observe() decorator (Python) and startActiveObservation (JS/TS) trace any function in your application, nesting LLM calls, tool calls, and retrieval steps into one trace. This is the structure that Helicone's flat request log and Helicone-Session-Path strings approximate.

Make a few requests and confirm in the Langfuse UI that each produces a trace with the model, token usage, and cost populated.

Parallel run and cutover. Keeping Helicone's gateway as base_url while the Langfuse wrapper traces the same calls gives you a comparison window with no double-capture risk: Helicone logs at the proxy, Langfuse at the client, one record each. Set an end date for the window; the cutover is pointing base_url back at the provider, and the rollback is removing the wrapper import.

If you already have OpenTelemetry instrumentation

Point your OTLP exporter at Langfuse's OTLP endpoint instead of adding an SDK:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64(public_key:secret_key)>,x-langfuse-ingestion-version=4"

Langfuse accepts OTLP over HTTP in both JSON and protobuf encodings (gRPC is not supported), and the x-langfuse-ingestion-version=4 header enables real-time visibility in the tracing table.

If you used Helicone primarily as a gateway

For multi-provider routing and failover, LiteLLM Proxy is a drop-in gateway replacement with native Langfuse export:

# litellm_config.yaml
litellm_settings:
  callbacks: ["langfuse_otel"]

This preserves the gateway pattern while routing all traces to Langfuse. See the LiteLLM Proxy integration guide for details.

Step 2: Map users, sessions, and properties

Helicone attaches attribution via request headers; Langfuse promotes the same concepts to first-class trace attributes, set at instrumentation time. user_id powers the user view with per-user cost and token usage (the Helicone-User-Id equivalent), session_id groups traces into sessions, and custom properties become metadata or tags:

from langfuse import observe, propagate_attributes

@observe()
def handle_request(question: str, user_id: str, conversation_id: str) -> str:
    # replaces Helicone-User-Id, Helicone-Session-Id, and Helicone-Property-* headers
    with propagate_attributes(
        user_id=user_id,
        session_id=conversation_id,
        tags=["support"],
        metadata={"plan": "enterprise"},
    ):
        ...  # traced client calls inherit these attributes
import { observe, propagateAttributes } from "@langfuse/tracing";

const handleRequest = observe(
  async (question: string, userId: string, conversationId: string) => {
    // replaces Helicone-User-Id, Helicone-Session-Id, and Helicone-Property-* headers
    return propagateAttributes(
      {
        userId,
        sessionId: conversationId,
        tags: ["support"],
        metadata: { plan: "enterprise" },
      },
      async () => {
        // traced client calls inherit these attributes
      },
    );
  },
);

Helicone-Session-Path hierarchies need no equivalent mapping: nesting comes from the instrumentation itself. Wrap each workflow step (/analysis, /analysis/security, ...) in its own observed function and the trace tree reproduces the hierarchy, with timings per step.

Confirm in the Langfuse UI that traces carry the user ID and session ID, and that the users and sessions views populate.

Step 3: Migrate prompts

Helicone offers two approaches to prompt management: a gateway approach where you pass a prompt_id and the gateway compiles the template server-side, and an SDK approach where you fetch and compile prompts client-side via @helicone/helpers (npm) / helicone-helpers (pip). Langfuse uses an SDK-based model similar to Helicone's SDK approach: prompts are fetched and compiled in your application code, with built-in caching to keep latency low and ensure guaranteed availability.

Export from Helicone and recreate in Langfuse

Helicone's prompt API exposes prompts, versions, bodies, and environment assignments. The script below walks all of them and recreates each version in Langfuse, oldest-first so Langfuse version numbers mirror the Helicone history, converting the variable syntax along the way and labeling the version that Helicone has deployed to production:

import os
import re

import requests
from langfuse import Langfuse

API = "https://api.helicone.ai"  # EU: https://eu.api.helicone.ai
HEADERS = {"Authorization": f"Bearer {os.environ['HELICONE_API_KEY']}"}

langfuse = Langfuse()


def to_langfuse_syntax(content: str) -> str:
    # {{ hc:customer_name:string }} -> {{customer_name}}
    return re.sub(r"\{\{\s*hc:(\w+):\w+\s*\}\}", r"{{\1}}", content)


prompts = []
page = 0
while True:
    batch = requests.post(
        f"{API}/v1/prompt-2025/query",
        headers=HEADERS,
        json={"page": page, "pageSize": 100},
    ).json()
    if not batch:
        break
    prompts.extend(batch)
    page += 1

for p in prompts:
    versions = requests.post(
        f"{API}/v1/prompt-2025/query/versions",
        headers=HEADERS,
        json={"promptId": p["id"]},
    ).json()

    # the version currently deployed to Helicone's production environment
    production = requests.post(
        f"{API}/v1/prompt-2025/query/environment-version",
        headers=HEADERS,
        json={"promptId": p["id"], "environment": "production"},
    ).json()
    production_id = production.get("id") if isinstance(production, dict) else None

    # oldest first, so Langfuse version numbers mirror Helicone's history
    for v in sorted(versions, key=lambda v: (v["major_version"], v["minor_version"])):
        body = requests.get(
            f"{API}/v1/prompt-2025/{v['id']}/prompt-body",
            headers=HEADERS,
        ).json()["data"]

        langfuse.create_prompt(
            name=p["name"],
            type="chat",
            prompt=[
                {"role": m["role"], "content": to_langfuse_syntax(m["content"])}
                for m in body["messages"]
            ],
            # model parameters and tools move into the prompt config
            config={
                k: body[k]
                for k in ("model", "temperature", "max_tokens", "tools")
                if body.get(k) is not None
            },
            labels=["production"] if v["id"] == production_id else [],
            commit_message=v.get("commit_message")
            or f"Helicone v{v['major_version']}.{v['minor_version']}",
        )

Prompt creation is not idempotent: every create_prompt call creates a new version. Run the export once, not as a retried batch job. To change labels afterwards, use langfuse.update_prompt(name=..., version=..., new_labels=[...]) instead of re-creating.

The mapping the script applies:

  • Name: Helicone's prompt name becomes the Langfuse prompt name.

  • Body split: Helicone stores the full request shape in one body. The messages array becomes the chat prompt content (see the prompt data model); model parameters and tool definitions move into the prompt config.

  • Variable syntax: Helicone's typed variables become plain Langfuse variables compiled at runtime via .compile(). If you relied on Helicone's type validation, move that check into your application code.

    HeliconeLangfuse
    {{hc:customer_name:string}}{{customer_name}}
    {{hc:is_premium:boolean}}{{is_premium}}
  • Environments → labels: the version deployed to Helicone's production environment gets the production label; map other environments (staging, development) to labels the same way.

  • Partials: Helicone prompt partials ({{hcp:prompt_id:index:environment}}) pull messages from other prompts. Recreate shared snippets as Langfuse text prompts referenced via composability, or insert message lists at runtime with message placeholders.

Confirm in the Langfuse UI that each prompt shows its full version history, variables render in the preview, and the production label sits on the right version.

Update application code

Replace Helicone's prompt resolution with Langfuse's fetch + compile flow.

If you used the gateway approach (prompt_id + inputs compiled server-side):

# Before (Helicone gateway)
response = client.chat.completions.create(
    model="gpt-4o-mini",
    extra_body={
        "prompt_id": "customer_support",
        "inputs": {"customer_name": "Alice", "issue_type": "billing"},
    },
)

# After (Langfuse)
from langfuse import Langfuse

langfuse = Langfuse()
prompt = langfuse.get_prompt("customer_support", label="production", type="chat")
compiled_messages = prompt.compile(customer_name="Alice", issue_type="billing")

response = client.chat.completions.create(
    model=prompt.config.get("model", "gpt-4o-mini"),
    messages=compiled_messages,
)

If you used the SDK approach (HeliconePromptManager from @helicone/helpers / helicone-helpers), the pattern is the same: one SDK fetch+compile swaps for another.

# Before (Helicone SDK)
# result = prompt_manager.get_prompt_body({
#     "prompt_id": "customer_support",
#     "inputs": {"customer_name": "Alice", "issue_type": "billing"},
# })
# response = client.chat.completions.create(**result["body"])

# After (Langfuse)
from langfuse import Langfuse

langfuse = Langfuse()
prompt = langfuse.get_prompt("customer_support", label="production", type="chat")
compiled_messages = prompt.compile(customer_name="Alice", issue_type="billing")

response = client.chat.completions.create(
    model=prompt.config.get("model", "gpt-4o-mini"),
    messages=compiled_messages,
)

See the prompt management get-started guide for full Python and TypeScript examples. To link traced generations to your migrated prompts, pass the prompt object to the generation as shown in linking prompts to traces.

Step 4: Decide what to do with historical requests

Export your Helicone request history early: with the vendor in maintenance mode, "export before you need it" is the safe default even if you never reingest most of it. Helicone ships a purpose-built export CLI with date filtering, full request/response bodies, and checkpoint-based crash recovery:

# JSONL export with full bodies (EU: add --region eu)
HELICONE_API_KEY="your-api-key" npx @helicone/export \
  --start-date 2026-01-01 --include-body --format jsonl

For programmatic exports, the underlying POST /v1/request/query-clickhouse endpoint returns the same records with request_id, request_created_at, user_id, custom properties, token counts, latency, and bodies.

Archive the export, then reingest selectively. Most teams keep history as an archive and start fresh: dashboards rebuild within days on live traffic, and reingested traces are billed as new ingestion. Reingest the subset you actively reference, such as requests you link from tickets, incident timelines, or eval datasets.

Reingest pattern: exported requests → OTLP spans

Transform exported records into OTLP spans and send them to Langfuse's OTLP endpoint. The rules that make this safe to run and to resume:

  • Preserve original timestamps. Set each span's start/end from request_created_at and latency; OTLP accepts historical timestamps, so traces sort correctly in time-range queries.
  • Derive deterministic IDs and checkpoint progress. Hash the Helicone request_id into the trace/span IDs so every source record maps to a stable Langfuse ID. Langfuse does not deduplicate re-ingested IDs, so checkpoint accepted source IDs and resume an interrupted import by skipping them.
  • Map the attribution fields. user_idlangfuse.user.id, session header values → langfuse.session.id, propertieslangfuse.trace.metadata.*, request/response bodies → langfuse.observation.input/langfuse.observation.output, and prompt_tokens/completion_tokenslangfuse.observation.usage_details so cost tracking works on the reingested generations.
  • Bound the window. Only reingest requests created before your Step 1 cutover time, so the historical import can't overlap live traces from the parallel-run window.
  • Reconcile counts. Compare the number of exported records against the observation count in Langfuse for the reingested window before deleting anything at the source.

Validation checklist

  • Live requests produce Langfuse traces with model, token usage, and cost populated
  • user_id and session_id appear on traces; users and sessions views populate
  • Custom properties are queryable as metadata or tags
  • Every Helicone prompt exists in Langfuse with its version history, and production labels sit on the deployed versions
  • Prompts resolve by name and label in application code, and variables compile correctly
  • Traced generations link to their prompt versions
  • Helicone request history exported and archived; selectively reingested traces reconciled by count
  • Dashboards and alerts recreated for the views your team actually used
  • Parallel-run window has an end date; Helicone gateway URL and headers removed at cutover

Limitations and gaps

Know these before you commit to the cutover:

  • Gateway features are out of scope. Multi-provider routing, failover, caching, and rate limiting are Helicone AI Gateway features, not Langfuse features. Call providers directly or run LiteLLM Proxy alongside Langfuse.
  • Typed prompt variables lose their types. {{hc:name:type}} validation moves into your application code; Langfuse variables are untyped strings at compile time.
  • Session paths require instrumentation. Helicone builds hierarchy from header strings; Langfuse builds it from nested spans, which means wrapping workflow steps in observed functions rather than adding a header.
  • Prompt import is not idempotent. Every create_prompt call creates a new version; run the export script once.
  • Historical reingest is billed as new ingestion. Import selectively rather than wholesale.

FAQ

Can I keep Helicone running during the transition?

Yes. The Langfuse SDK wraps the client inside your application while Helicone's gateway sits in the request path, so both capture the same call once, independently. Keep the gateway base_url during your validation window, compare the two systems, then point base_url back at the provider. The Helicone integration page covers running the two together in more detail.

I use Helicone's EU region. Anything different?

Use eu.api.helicone.ai for the prompt and request export APIs and --region eu on the export CLI. On the Langfuse side, pick the region when creating your project; LANGFUSE_BASE_URL is https://cloud.langfuse.com (EU) or https://us.cloud.langfuse.com (US), or your own domain when self-hosting.

Do I still need a gateway at all?

Not for observability: Langfuse SDKs capture traces client-side and export them in the background, off the request path. If you used Helicone's gateway for routing or failover, keep a gateway for that job (LiteLLM Proxy integrates natively with Langfuse); if the gateway was only there for logging, drop it.

What happens to my Helicone dashboards and saved views?

Rebuild them on live data: the observations table supports the same filtering (by user, metadata, model) via the filter search bar, and custom dashboards cover cost, latency, and usage breakdowns. The Metrics API serves programmatic consumers.

Get help with the migration

Start on Langfuse Cloud or self-host. If you want a migration plan, talk to us.


Was this page helpful?