> ## Documentation Index
> Fetch the complete documentation index at: https://docs.honeyhive.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# How to migrate from Langfuse to HoneyHive

> HoneyHive migration guide from Langfuse. Switch tracing, datasets, evaluators, and prompts to HoneyHive with OTLP or the Python SDK.

Migrate your Langfuse app to HoneyHive by switching tracing, importing datasets, and recreating the prompts your app uses.

These examples use Langfuse Python SDK v4 (`from langfuse import observe`). If your app imports `langfuse.decorators` or `langfuse.callback`, see [older Langfuse SDK versions](#older-langfuse-sdk-versions).

<Accordion title="Use a coding agent to migrate">
  ```text theme={null}
  Use this HoneyHive migration guide: https://docs.honeyhive.ai/v2/tracing/migrate-from-langfuse.md

  Explore my codebase to find Langfuse tracing, scoring, datasets, and prompts. Ask any questions needed to understand the project and runtime. Then present a concise migration plan for my confirmation. After I confirm the plan, implement the changes and verify that traces reach HoneyHive.
  ```
</Accordion>

## Choose a path

Get the project-scoped API key for the HoneyHive project that receives your traces.

Go to [**Settings > Project > API Keys**](https://app.us.honeyhive.ai/settings/project/keys), open the **Project** tab, and click **Create API Key**. Copy the key from the dialog and store it as `HH_API_KEY` - it is only shown once.

This flow runs experiments and reads results, so it needs a project API key. To send its traces with an [ingestion key](/v2/workspace/api-keys#ingestion-keys) instead, also set `HH_INGESTION_API_KEY`; the SDK uses each key where it applies.

* **[Path A](#path-a-swap-the-python-sdk):** start here if your Python app calls `@observe`, `langfuse.openai`, or `CallbackHandler`.
* **[Path B](#path-b-send-opentelemetry-spans-to-honeyhive):** use this path if your app already emits OpenTelemetry, OpenInference, OpenLLMetry, or OTel GenAI spans. Non-Python apps use this path.

## Path A: swap the Python SDK

Replace each Langfuse integration in your Python app.

### 1. Install HoneyHive

HoneyHive requires Python 3.11 or newer.

Run these commands from your app's directory. If it already has an active virtual environment, skip the first two commands.

```bash theme={null}
uv venv
source .venv/bin/activate
uv pip install "honeyhive[openinference-openai]" openai
uv pip check
```

Install only the instrumentors your app uses. For a LangChain app, install `"honeyhive[openinference-langchain]"`. See the [integrations overview](/v2/integrations/overview) for other libraries.

```bash theme={null}
export HH_API_KEY="your-honeyhive-api-key"

# Optional for self-hosted or dedicated HoneyHive
# export HH_API_URL="https://api.your-instance.honeyhive.ai"
```

<Tip>
  To see where to initialize the tracer for your environment, including AWS Lambda and long-running servers, see [Tracer Initialization](/v2/tracing/tracer-initialization).
</Tip>

### 2. Replace `@observe` with `@trace`

<CodeGroup>
  ```python Langfuse theme={null}
  from langfuse import get_client, observe, propagate_attributes
  from openai import OpenAI

  client = OpenAI()

  @observe()
  def generate_response(prompt: str, user_id: str) -> str:
      with propagate_attributes(user_id=user_id, tags=["support"]):
          response = client.chat.completions.create(
              model="gpt-4o-mini",
              messages=[{"role": "user", "content": prompt}],
          )
          return response.choices[0].message.content

  generate_response("Hello", "user-123")
  get_client().flush()
  ```

  ```python HoneyHive theme={null}
  import os

  from honeyhive import HoneyHiveTracer, trace
  from openai import OpenAI
  from openinference.instrumentation.openai import OpenAIInstrumentor

  tracer = HoneyHiveTracer.init(
      api_key=os.getenv("HH_API_KEY"),
      source="development",
      session_name="generate-response",
  )
  OpenAIInstrumentor().instrument(tracer_provider=tracer.provider)

  client = OpenAI()

  @trace(event_type="chain", event_name="generate_response")
  def generate_response(prompt: str, user_id: str) -> str:
      tracer.enrich_session(
          user_properties={"user_id": user_id},
          metadata={"tags": ["support"]},
      )
      response = client.chat.completions.create(
          model="gpt-4o-mini",
          messages=[{"role": "user", "content": prompt}],
      )
      return response.choices[0].message.content

  generate_response("Hello", "user-123")
  tracer.flush()
  ```
</CodeGroup>

`OpenAIInstrumentor` traces the OpenAI call without `@trace`. Keep `@trace` when you want to preserve the Langfuse `@observe()` function as a parent `chain` event with its own inputs and output.

Run the HoneyHive version and inspect the session. Confirm the parent chain event, the model call under it, and the session user ID and tags.

`enrich_session()` applies to the current HoneyHive session rather than a `with` block. If you used a Langfuse `session_id` for a multi-turn conversation, [create or reuse a HoneyHive session](/v2/tracing/tracer-initialization#multi-turn-conversations) for that conversation.

Keep `tracer.flush()` at the end of scripts, notebooks, and Lambda handlers. Long-running servers flush on a timer.

### 3. Replace nested observations

<CodeGroup>
  ```python Langfuse theme={null}
  from langfuse import get_client, observe

  langfuse = get_client()

  @observe()
  def process_document(doc: str) -> str:
      langfuse.update_current_span(metadata={"doc_length": len(doc)})

      with langfuse.start_as_current_observation(
          as_type="span", name="extract_entities"
      ) as span:
          entities = extract(doc)
          span.update(output={"entities": entities})

      with langfuse.start_as_current_observation(
          as_type="generation", name="summarize"
      ) as generation:
          summary = summarize(doc, entities)
          generation.update(output=summary)

      return summary

  process_document("HoneyHive records nested traces.")
  langfuse.flush()
  ```

  ```python HoneyHive theme={null}
  import os

  from honeyhive import HoneyHiveTracer, trace
  from honeyhive.tracer.processing.context import enrich_span_context

  tracer = HoneyHiveTracer.init(api_key=os.getenv("HH_API_KEY"))

  @trace(event_type="chain", event_name="process_document")
  def process_document(doc: str) -> str:
      tracer.enrich_span(metadata={"doc_length": len(doc)})

      with enrich_span_context(
          event_name="extract_entities",
          attributes={"honeyhive_event_type": "chain"},
          inputs={"doc": doc},
          tracer_instance=tracer,
      ):
          entities = extract(doc)
          tracer.enrich_span(outputs={"entities": entities})

      with enrich_span_context(
          event_name="summarize",
          attributes={"honeyhive_event_type": "model"},
          inputs={"doc": doc},
          tracer_instance=tracer,
      ):
          summary = summarize(doc, entities)
          tracer.enrich_span(outputs={"summary": summary})

      return summary

  process_document("HoneyHive records nested traces.")
  tracer.flush()
  ```
</CodeGroup>

Map each Langfuse `span` to a `chain` event, each `generation` to a `model` event, and each tool call to a `tool` event.

When you call `enrich_span_context()`, pass `tracer_instance=tracer` and set `attributes={"honeyhive_event_type": "chain"}` (or `"model"` / `"tool"`). There is no `event_type` argument. You can also add `@trace` to child functions. See [custom spans](/v2/tracing/custom-spans) for both patterns.

### 4. Replace the Langfuse OpenAI client

<CodeGroup>
  ```python Langfuse theme={null}
  from langfuse.openai import OpenAI

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

  ```python HoneyHive theme={null}
  import os

  from honeyhive import HoneyHiveTracer
  from openai import OpenAI
  from openinference.instrumentation.openai import OpenAIInstrumentor

  tracer = HoneyHiveTracer.init(api_key=os.getenv("HH_API_KEY"))
  OpenAIInstrumentor().instrument(tracer_provider=tracer.provider)

  client = OpenAI()
  response = client.chat.completions.create(
      model="gpt-4o-mini",
      messages=[{"role": "user", "content": "Hello"}],
  )
  tracer.flush()
  ```
</CodeGroup>

Initialize the tracer, instrumentor, and standard OpenAI client in that order. OpenInference records each OpenAI call as a model event. See the [OpenAI integration](/v2/integrations/openai) for configuration options.

<Warning>
  Do not wrap the same client with `langfuse.openai` and `OpenAIInstrumentor`. Both would record the call.
</Warning>

### 5. Replace the LangChain callback

<CodeGroup>
  ```python Langfuse theme={null}
  from langchain_core.prompts import ChatPromptTemplate
  from langchain_openai import ChatOpenAI
  from langfuse import get_client, propagate_attributes
  from langfuse.langchain import CallbackHandler

  handler = CallbackHandler()
  llm = ChatOpenAI(model="gpt-4o-mini")
  prompt = ChatPromptTemplate.from_template("Tell me a joke about {topic}")
  chain = prompt | llm

  with propagate_attributes(user_id="user-123"):
      chain.invoke(
          {"topic": "bees"},
          config={"callbacks": [handler]},
      )

  get_client().flush()
  ```

  ```python HoneyHive theme={null}
  import os

  from honeyhive import HoneyHiveTracer
  from langchain_core.prompts import ChatPromptTemplate
  from langchain_openai import ChatOpenAI
  from openinference.instrumentation.langchain import LangChainInstrumentor

  tracer = HoneyHiveTracer.init(
      api_key=os.getenv("HH_API_KEY"),
      session_name="langchain-call",
  )
  LangChainInstrumentor().instrument(tracer_provider=tracer.provider)

  llm = ChatOpenAI(model="gpt-4o-mini")
  prompt = ChatPromptTemplate.from_template("Tell me a joke about {topic}")
  chain = prompt | llm

  tracer.enrich_session(user_properties={"user_id": "user-123"})
  chain.invoke({"topic": "bees"})
  tracer.flush()
  ```
</CodeGroup>

Instrument LangChain once, then remove the callback argument from each invocation. See the [LangChain](/v2/integrations/langchain) and [LangGraph](/v2/integrations/langgraph) guides for configuration options.

<Warning>
  Do not attach a Langfuse `CallbackHandler` and a HoneyHive `LangChainInstrumentor` to the same call. Both would record the invocation. To send spans to both products during migration, use two exporters in an OpenTelemetry Collector.
</Warning>

## Path B: send OpenTelemetry spans to HoneyHive

Keep your existing instrumentation and point its standard OTLP/HTTP exporter at HoneyHive.

### 1. Configure the exporter

HoneyHive receives OTLP/HTTP traces at:

```text theme={null}
https://<provider-host>/opentelemetry/v1/traces
```

Set your HoneyHive API key and the exporter variables. The default US host is `api.dp1.us.honeyhive.ai`:

```bash theme={null}
export HH_API_KEY="your-honeyhive-api-key"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.dp1.us.honeyhive.ai/opentelemetry"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${HH_API_KEY}"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
```

The OTLP/HTTP exporter adds `/v1/traces` to `OTEL_EXPORTER_OTLP_ENDPOINT`. If your exporter expects a traces-only URL, set the full path:

```bash theme={null}
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://api.dp1.us.honeyhive.ai/opentelemetry/v1/traces"
```

Use the API host shown in your HoneyHive dashboard if it differs. If your exporter can't send `http/protobuf`, send OTLP/HTTP JSON.

<Warning>
  `OTEL_EXPORTER_*` variables do not retarget `LangfuseSpanProcessor`. Replace that processor with an OTLP/HTTP exporter, or send the spans through an OpenTelemetry Collector.
</Warning>

Langfuse v4 filters spans to LLM-focused instrumentation by default. A generic OTLP exporter can also send HTTP, database, queue, and framework spans. Recreate your existing filter in the SDK or Collector if you want to keep the same span set.

To send spans to both products during migration, configure two exporters in your OpenTelemetry Collector.

### 2. Send a request

Run one request through your app, then open [Traces](https://app.us.honeyhive.ai/traces/sessions) in HoneyHive. Check the trace hierarchy, inputs, outputs, and event types.

HoneyHive maps OpenInference and OTel GenAI fields (inputs, outputs, model, tokens, span kind) to native fields. Other OTLP attributes, including `langfuse.*` attributes, remain in metadata but are not translated into HoneyHive fields. Remap the values you still need, and recreate Langfuse concepts such as `public` and `bookmarked` separately. See [API mappings](#api-mappings) for fields that have a HoneyHive equivalent.

## Move scores and evaluators

Write Langfuse scores as HoneyHive metrics on the current span or session:

<CodeGroup>
  ```python Langfuse theme={null}
  from langfuse import get_client

  langfuse = get_client()
  langfuse.create_score(
      name="quality",
      value=0.8,
      trace_id="...",
      data_type="NUMERIC",
  )
  ```

  ```python HoneyHive theme={null}
  # Attach the score to the current event.
  tracer.enrich_span(metrics={"quality": 0.8})

  # Or attach it once at the session level.
  tracer.enrich_session(metrics={"quality": 0.8})
  ```
</CodeGroup>

Move each automated or human check to the matching HoneyHive feature:

| Existing check                        | HoneyHive                                                   |
| ------------------------------------- | ----------------------------------------------------------- |
| Guardrail or format check in app code | [`enrich_span(metrics=...)`](/v2/tracing/client-side-evals) |
| LLM judge on production traces        | [Server-side LLM evaluator](/v2/evaluators/llm)             |
| Dataset experiment                    | [`evaluate(dataset_id=...)`](/v2/datasets/run-experiments)  |
| Human review queue                    | [Annotation queue](/v2/evaluation/annotation-queues)        |

After you import a dataset, run your latest baseline with [`evaluate(dataset_id=...)`](/v2/datasets/run-experiments). Use the same judge prompt, model, and settings for comparison.

To keep migrated evaluators and datasets in version control, follow [Config as Code](/v2/cli-reference/config-as-code) and apply them with the HoneyHive CLI or CI.

## Move datasets

For text and structured data, export your Langfuse dataset as CSV, then [upload it to HoneyHive](/v2/datasets/import). Confirm that the row count matches before you run an experiment. For multimodal datasets, export media separately and replace Langfuse media references with values your application can load.

<Accordion title="Copy a dataset with the Python SDK">
  ```python theme={null}
  import os

  from honeyhive import HoneyHive
  from honeyhive.models import (
      AddDatapointsToDatasetRequest,
      CreateDatasetRequest,
      DatapointMapping,
  )
  from langfuse import get_client


  def as_object(value, fallback_key):
      if isinstance(value, dict):
          return value
      return {fallback_key: value}


  def unique_keys(records):
      keys = []
      for record in records:
          for key in record:
              if key not in keys:
                  keys.append(key)
      return keys


  def rename_until_free(name, taken, prefix):
      mapped = name
      while mapped in taken:
          mapped = f"{prefix}{mapped}"
      taken.add(mapped)
      return mapped


  langfuse = get_client()
  hh = HoneyHive(api_key=os.environ["HH_API_KEY"])

  source = langfuse.get_dataset("golden-set")
  created = hh.datasets.create(
      CreateDatasetRequest(
          name="golden-set",
          description="Imported from Langfuse",
      )
  )
  dataset_id = created.result.insertedId

  items = list(source.items)
  input_records = [as_object(item.input, "input") for item in items]
  ground_truth_records = [
      as_object(item.expected_output, "expected_output")
      if item.expected_output is not None
      else {}
      for item in items
  ]

  input_keys = unique_keys(input_records)
  ground_truth_keys = unique_keys(ground_truth_records)

  taken = set(input_keys)
  ground_truth_key_map = {
      key: rename_until_free(key, taken, "expected_")
      for key in ground_truth_keys
  }
  source_metadata_key = rename_until_free("langfuse_source", taken, "_")

  rows = []
  for item, inputs, ground_truth in zip(
      items, input_records, ground_truth_records
  ):
      row = {key: inputs.get(key) for key in input_keys}
      row.update(
          {
              ground_truth_key_map[key]: ground_truth.get(key)
              for key in ground_truth_keys
          }
      )
      row[source_metadata_key] = {
          "item_id": item.id,
          "metadata": item.metadata or {},
      }
      rows.append(row)

  if rows:
      hh.datasets.add_datapoints(
          dataset_id,
          AddDatapointsToDatasetRequest(
              data=rows,
              mapping=DatapointMapping(
                  inputs=input_keys,
                  ground_truth=list(ground_truth_key_map.values()),
              ),
          ),
      )
  ```

  <Warning>
    This script creates a new HoneyHive dataset every time it runs. Before you run it, confirm that the destination name is unused or choose a unique name.

    HoneyHive dataset names allow letters, numbers, spaces, `_`, `-`, `'`, and `&`. Replace characters such as `/` if you reuse a Langfuse dataset name.
  </Warning>
</Accordion>

## Move prompts

Use the Langfuse and HoneyHive Python SDKs to copy each prompt version you still use. HoneyHive stores every template as a message array, so the script converts a Langfuse text prompt to one `user` message. It preserves the remaining Langfuse configuration values without requiring manual Playground entry.

Use this label mapping when you deploy:

| Langfuse label           | HoneyHive environment                                   |
| ------------------------ | ------------------------------------------------------- |
| `production` or no label | `prod`                                                  |
| `staging`                | `staging`                                               |
| `latest`                 | No direct mapping; deploy a selected version explicitly |
| Custom label             | Choose `dev`, `staging`, or `prod`                      |

Langfuse moves the `latest` label whenever you create a version. HoneyHive environments point to configurations you deploy explicitly, so choose which version reaches `dev`, `staging`, or `prod`.

<Accordion title="Copy a prompt version with the Python SDK">
  Set the source prompt and destination configuration:

  ```bash theme={null}
  export LANGFUSE_PROMPT_NAME="my-prompt"
  export LANGFUSE_PROMPT_VERSION="3"
  export LANGFUSE_PROMPT_TYPE="chat"
  export HH_PROMPT_ENVS="prod"
  export HH_PROMPT_PROVIDER="openai"
  export HH_PROMPT_MODEL="gpt-4o-mini"
  ```

  `HH_PROMPT_MODEL` is a fallback for Langfuse prompts whose `config` does not include a model.

  ```python theme={null}
  import hashlib
  import os
  import re

  from honeyhive import HoneyHive
  from honeyhive.models import CreateConfigurationRequest
  from langfuse import get_client


  def valid_honeyhive_name(name: str) -> str:
      cleaned = re.sub(r"[^a-zA-Z0-9 _\-'&]", "-", name).strip()
      if cleaned == name and len(cleaned) <= 200:
          return cleaned

      digest = hashlib.sha256(name.encode()).hexdigest()[:8]
      suffix = f"-{digest}"
      return f"{cleaned[: 200 - len(suffix)]}{suffix}"


  prompt_name = os.environ["LANGFUSE_PROMPT_NAME"]
  prompt_version = int(os.environ["LANGFUSE_PROMPT_VERSION"])
  prompt_type = os.environ.get("LANGFUSE_PROMPT_TYPE", "text")
  destination_name = valid_honeyhive_name(f"{prompt_name}-v{prompt_version}")
  destination_envs = [
      env.strip()
      for env in os.environ.get("HH_PROMPT_ENVS", "dev").split(",")
      if env.strip()
  ]
  invalid_envs = set(destination_envs) - {"dev", "staging", "prod"}
  if not destination_envs or invalid_envs:
      raise ValueError(f"Invalid HoneyHive environments: {sorted(invalid_envs)}")

  langfuse = get_client()
  honeyhive = HoneyHive(api_key=os.environ["HH_API_KEY"])

  source = langfuse.get_prompt(
      prompt_name,
      version=prompt_version,
      type=prompt_type,
  )
  source_config = dict(source.config or {})
  model = source_config.pop("model", None) or os.environ["HH_PROMPT_MODEL"]

  if prompt_type == "chat":
      placeholders = [
          item["name"]
          for item in source.prompt
          if item.get("type") == "placeholder"
      ]
      if placeholders:
          raise ValueError(
              "Map Langfuse message placeholders in application code before "
              f"migrating: {placeholders}"
          )
      template = [
          {"role": item["role"], "content": item["content"]}
          for item in source.prompt
      ]
  else:
      template = [{"role": "user", "content": source.prompt}]

  if any(config.name == destination_name for config in honeyhive.configurations.list()):
      raise RuntimeError(f"Configuration already exists: {destination_name}")

  created = honeyhive.configurations.create(
      CreateConfigurationRequest(
          name=destination_name,
          type="LLM",
          provider=os.environ["HH_PROMPT_PROVIDER"],
          parameters={
              "call_type": "chat" if prompt_type == "chat" else "completion",
              "model": model,
              "template": template,
              "hyperparameters": source_config,
          },
          env=destination_envs,
          tags=["migrated-from-langfuse"],
          user_properties={
              "langfuse_prompt_name": prompt_name,
              "langfuse_prompt_version": prompt_version,
              "langfuse_prompt_type": prompt_type,
          },
      )
  )
  print(created.insertedId)
  ```

  The script stores the remaining Langfuse `config` fields in `hyperparameters`, including provider-specific fields such as tools, `tool_choice`, and response schemas. Your application can forward those fields to its model client. To use tools or response formats in the HoneyHive Playground, map them to the native `selectedFunctions`, `functionCallParams`, `forceFunction`, and `responseFormat` parameters.

  Run the script once per prompt version. Set `HH_PROMPT_ENVS` to every HoneyHive environment that version serves, such as `staging,prod`. Keep one configuration per prompt version instead of creating duplicate configurations with the same name. To promote a different version later, update the `env` lists on the affected configurations.

  Langfuse message placeholders have no direct HoneyHive configuration equivalent. The script stops when it finds one so you can insert those runtime messages in application code instead of silently dropping them.
</Accordion>

<Accordion title="Use a coding agent to migrate application prompts">
  Paste this prompt into your coding agent:

  ```text theme={null}
  Migrate this application's Langfuse prompts to HoneyHive programmatically.

  1. Find every langfuse.get_prompt(...) call and record its prompt name, type,
     label or version, and how prompt.config is passed to the model provider.
  2. Resolve each moving Langfuse label to an explicit version before migrating.
  3. Use the Python SDK migration example at
     https://docs.honeyhive.ai/v2/tracing/migrate-from-langfuse#move-prompts to
     create one HoneyHive configuration per retained version. Convert text
     prompts to one user message, preserve chat messages and provider-specific
     config fields, and handle message placeholders in application code.
  4. Map production, staging, and custom labels to explicit HoneyHive dev,
     staging, or prod environment lists. Do not map Langfuse latest
     automatically.
  5. Refuse to overwrite an existing HoneyHive configuration. Show me the
     source-to-destination mapping before creating anything.
  6. Update the application to fetch the deployed HoneyHive configuration,
     preserve its current prompt rendering and model arguments, and verify one
     request end to end.
  ```
</Accordion>

If Langfuse resolves `langfuse.get_prompt("my-prompt", label="production")` to version 3, find `my-prompt-v3` in `honeyhive.configurations.list()` with `prod` in its `env` list:

```python theme={null}
configuration = next(
    config
    for config in honeyhive.configurations.list()
    if config.name == "my-prompt-v3" and "prod" in config.env
)
template = configuration.parameters.template

if configuration.parameters.call_type == "completion":
    prompt_text = template[0].content
else:
    messages = [message.model_dump() for message in template]
```

Pass `prompt_text` or `messages`, plus `configuration.parameters.hyperparameters`, to the corresponding model client.

## Check that it worked

1. Send traffic that covers a normal request, a tool call, and an error.
2. Open [Traces](https://app.us.honeyhive.ai/traces/sessions) in the project that owns your `HH_API_KEY`.
3. Confirm the span hierarchy, model name, token usage, user ID, and custom metadata.
4. Compare the dataset row count with Langfuse, then run one baseline experiment with `evaluate(dataset_id=...)`.

After these checks pass, remove Langfuse:

```bash theme={null}
uv pip uninstall langfuse
unset LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY LANGFUSE_BASE_URL LANGFUSE_HOST
```

Delete Langfuse imports such as `observe`, `get_client`, `CallbackHandler`, and `langfuse.openai`. Keep `tracer.flush()` in scripts and other short-lived processes.

## Troubleshooting

### Traces do not appear

1. Confirm `HH_API_KEY` is set and belongs to the HoneyHive project you opened.
2. Call `HoneyHiveTracer.init(...)` before `instrument()` and before you create model clients.
3. Pass `tracer_provider=tracer.provider` to the instrumentor.
4. Set `HH_API_URL` or pass `server_url=` to `HoneyHiveTracer.init` when you use self-hosted HoneyHive.
5. Call `tracer.flush()` before a script, notebook, or Lambda handler exits.

### OTLP environment variables have no effect

Remove `LangfuseSpanProcessor` and configure a standard OTLP exporter or OpenTelemetry Collector. `OTEL_EXPORTER_*` variables do not change where the Langfuse processor sends spans.

### Nested spans are missing

Decorate child functions with `@trace` or wrap blocks in `enrich_span_context()`. A parent `@trace` does not create child spans around undecorated calls.

### OpenAI calls are duplicated

Remove `from langfuse.openai import openai` or `OpenAI` before you enable `OpenAIInstrumentor`. Use the standard `openai` package.

### LangChain calls appear twice

Remove the Langfuse `CallbackHandler` after you enable `LangChainInstrumentor`. Do not attach both to one invocation.

### Dataset rows appear in metadata

Make sure `DatapointMapping.inputs` and `ground_truth` exactly match keys in each row. HoneyHive stores unmapped keys as metadata.

## Appendix

### API mappings

Use these tables to replace other Langfuse calls in your app.

| Langfuse                                                                | HoneyHive                                                                               |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Project                                                                 | Project selected by the project-scoped `HH_API_KEY`                                     |
| Trace                                                                   | [Session](/v2/tracing/concepts)                                                         |
| Observation (`generation` or `embedding`)                               | Event (`model`)                                                                         |
| Observation (`agent`, `chain`, or `span`)                               | Event (`chain`)                                                                         |
| Observation (`tool`, `retriever`, `event`, `evaluator`, or `guardrail`) | Event (`tool`)                                                                          |
| Score                                                                   | `metrics` on a span or session, or a [server-side evaluator](/v2/evaluators/llm)        |
| Dataset item `input` and `expected_output`                              | Datapoint `inputs` and `ground_truth`                                                   |
| Prompt name and label                                                   | Prompt configuration name and environment (`dev`, `staging`, or `prod`)                 |
| `user_id`, `tags`                                                       | `user_properties`, `metadata`                                                           |
| `session_id`                                                            | [HoneyHive session context](/v2/tracing/tracer-initialization#multi-turn-conversations) |
| `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY`                         | One project-scoped `HH_API_KEY`                                                         |

| Langfuse Python                                  | HoneyHive Python                                                                                                                              |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `@observe()`                                     | `@trace()`                                                                                                                                    |
| `start_as_current_observation(...)`              | [`enrich_span_context()`](/v2/tracing/custom-spans) or nested `@trace`                                                                        |
| `update_current_span(metadata=...)`              | `tracer.enrich_span(metadata=...)`                                                                                                            |
| `span.update(output=...)`                        | `tracer.enrich_span(outputs=...)`                                                                                                             |
| `propagate_attributes(user_id=...)`              | `tracer.enrich_session(user_properties={"user_id": ...})`                                                                                     |
| `propagate_attributes(session_id=...)`           | Create or reuse a [HoneyHive session](/v2/tracing/tracer-initialization#multi-turn-conversations) and keep the old ID in `metadata` if needed |
| `create_score(...)` or `span.score(...)`         | `tracer.enrich_span(metrics=...)` or a [server-side evaluator](/v2/evaluators/llm)                                                            |
| `from langfuse.openai import OpenAI`             | Standard `OpenAI()` with `OpenAIInstrumentor`                                                                                                 |
| `from langfuse.langchain import CallbackHandler` | `LangChainInstrumentor().instrument(...)`                                                                                                     |
| `get_client().flush()`                           | `tracer.flush()` in scripts, notebooks, and Lambda handlers                                                                                   |
| `LANGFUSE_SAMPLE_RATE`                           | Application-level [sampling](/v2/tracing/sampling)                                                                                            |

### Older Langfuse SDK versions

For Langfuse Python SDK v2 and v3, either update to v4 first or replace the older imports directly:

| Older SDK                                                         | Current Langfuse v4                                                                                                                                                                             |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from langfuse.decorators import observe, langfuse_context` in v2 | `from langfuse import observe, get_client, propagate_attributes`                                                                                                                                |
| `from langfuse.callback import CallbackHandler` in v2             | `from langfuse.langchain import CallbackHandler`                                                                                                                                                |
| `langfuse_context.update_current_observation(...)` in v2          | `get_client().update_current_span(...)`, or `update_current_generation(...)` for model, usage, cost, and prompt fields                                                                          |
| `update_current_trace(...)` in v3                                 | Split correlating attributes into `propagate_attributes(...)`, trace I/O into `set_current_trace_io(...)`, visibility into `set_current_trace_as_public()`, and release into `LANGFUSE_RELEASE` |
| Deprecated `LANGFUSE_HOST` in v2 and earlier v3 releases          | `LANGFUSE_BASE_URL`                                                                                                                                                                             |
| `langfuse_context.flush()`                                        | `get_client().flush()`                                                                                                                                                                          |

### Optional historical backfill

HoneyHive does not provide a Langfuse importer. To backfill selected historical traces, recreate them with the current [Sessions](/v2/api-reference-autogen/sessions/start-a-new-session) and [Events](/v2/api-reference-autogen/events/create-a-new-event) APIs. Create new HoneyHive session and event UUIDs, and keep the original Langfuse IDs in `metadata`.

Request `fields=core,basic,time,io,metadata,model,usage,trace_context` from the [Langfuse Observations API v2](https://langfuse.com/docs/api-and-data-platform/features/observations-api). The endpoint returns only `core,basic` by default. Set `expandMetadata` to a comma-separated list of metadata keys whose values must not be truncated at 200 characters. Bound each request with `fromStartTime` and `toStartTime`, then follow `meta.cursor` until it is empty.

Export scores separately with `fields=details,subject` from the [Langfuse Scores API v3](https://langfuse.com/docs/api-and-data-platform/features/scores-api), following its cursor until it is empty. The Observations API does not return scores.

| Langfuse object | HoneyHive object                                                                               | Endpoint                                 |
| --------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------- |
| Trace           | Session                                                                                        | `POST /v1/sessions`                      |
| Observation     | Event (`model`, `chain`, or `tool`)                                                            | `POST /v1/events`                        |
| Score           | Event `metrics` plus metadata for an observation subject; session metadata for a trace subject | `POST /v1/events` or `POST /v1/sessions` |

<Accordion title="Trace, observation, and score field mappings">
  #### Trace to session

  The Langfuse Observations API repeats trace context on each observation. Create one HoneyHive session per unique `traceId` and use fields from the root observation.

  | Langfuse                                 | HoneyHive                        |
  | ---------------------------------------- | -------------------------------- |
  | `traceId`                                | `metadata.langfuse_trace_id`     |
  | `traceName`                              | `session_name`                   |
  | `userId`                                 | `user_properties.user_id`        |
  | `sessionId`                              | `metadata.langfuse_session_id`   |
  | Root observation `input` and `output`    | `inputs` and `outputs`           |
  | `metadata`, `tags`, `version`, `release` | `metadata`                       |
  | Set during import                        | `source` with value `"langfuse"` |

  `POST /v1/sessions` does not accept `external_id`. Store the Langfuse trace ID in session metadata.

  #### Generation to model event

  | Langfuse                                      | HoneyHive                                                         |
  | --------------------------------------------- | ----------------------------------------------------------------- |
  | `traceId`                                     | `session_id` through your trace-to-session map                    |
  | `id`                                          | `metadata.langfuse_observation_id`                                |
  | `parentObservationId`                         | `parent_id` through your observation-to-event map                 |
  | `name`                                        | `event_name`                                                      |
  | `model`                                       | `config.model`                                                    |
  | `modelParameters`                             | `config.hyperparameters`                                          |
  | `input`                                       | `inputs`                                                          |
  | `output`                                      | `outputs`                                                         |
  | `usageDetails.input`, `.output`, and `.total` | `metadata.prompt_tokens`, `completion_tokens`, and `total_tokens` |
  | `costDetails.total` or `totalCost`            | `metadata.cost`                                                   |
  | `startTime` and `endTime`                     | Unix timestamps in milliseconds                                   |
  | `statusMessage` when `level` is `"ERROR"`     | `error`                                                           |

  The Langfuse Observations API v2 returns input and output as raw strings. Decode valid JSON first. Keep decoded objects as they are. Wrap arrays, scalars, and plain strings in a named field such as `{"value": ...}`. HoneyHive session and event inputs and outputs must be objects, and event `inputs` is required.

  #### Observation type to event type

  | Langfuse `type`                                        | HoneyHive `event_type` |
  | ------------------------------------------------------ | ---------------------- |
  | `GENERATION`, `EMBEDDING`                              | `model`                |
  | `AGENT`, `CHAIN`, `SPAN`                               | `chain`                |
  | `TOOL`, `RETRIEVER`, `EVENT`, `EVALUATOR`, `GUARDRAIL` | `tool`                 |

  #### Scores

  `POST /v1/sessions` does not accept top-level `metrics` or `feedback`, but `POST /v1/events` accepts `metrics`. For an observation-subject score, put the selected numeric or boolean value in the mapped event's `metrics` object so HoneyHive can filter and chart it. Preserve the complete score record under `metadata.langfuse_scores[]`, including `id`, `name`, `value`, `data_type`, `subject`, `comment`, and `source`. For a trace-subject score, keep that record in the mapped session's metadata. Langfuse session and experiment subjects have no one-to-one HoneyHive backfill target, so preserve their complete `subject` object and choose the corresponding imported session or experiment workflow. For live traffic, use `tracer.enrich_span(metrics=...)` or `tracer.enrich_session(metrics=...)`.

  #### Data to recreate

  Langfuse `public`, `bookmarked`, prompt templates, dataset run history, and annotation queues do not map to historical session or event fields. Recreate the prompts, datasets, and review workflows you still use.
</Accordion>

## Related

<CardGroup cols={2}>
  <Card title="Tracing quickstart" icon="rocket" href="/v2/introduction/tracing-quickstart">
    Send a first OpenAI trace with OpenInference
  </Card>

  <Card title="Custom spans" icon="brackets-curly" href="/v2/tracing/custom-spans">
    Replace @observe with @trace and context managers
  </Card>

  <Card title="LangChain integration" icon="link" href="/v2/integrations/langchain">
    Instrument LangChain and LangGraph without a callback handler
  </Card>

  <Card title="Run an experiment" icon="flask" href="/v2/datasets/run-experiments">
    Re-run a baseline with evaluate(dataset\_id=...) after you import a dataset
  </Card>
</CardGroup>

<Note>
  Need help migrating from self-hosted Langfuse? [Contact us](mailto:sales@honeyhive.ai).
</Note>


## Related topics

- [AI Application Tracing with HoneyHive](/v2/tracing/introduction.md)
- [How to trace OpenAI with HoneyHive](/v2/integrations/openai.md)
- [How to trace LangChain with HoneyHive](/v2/integrations/langchain.md)
