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

# Tracing

> Record your app's LLM calls to a Freesolo project, then export them as a training dataset.

Tracing records the chat completions your app already makes and stores them as
traces on a Freesolo **project**. Point your existing OpenAI-compatible client at
Freesolo's recording endpoint, let it record while your app runs, then pull the
traces down with [`flash traces export`](/reference/cli#traces) as
[task records](/guides/datasets#task-records) - real production traffic becomes
the `dataset/train.jsonl` you train on.

<Note>
  Tracing needs an existing **project id**: traces are stored per-project, and a
  recording request without one is rejected rather than stored nowhere. Create a
  project in the [dashboard](/platform) or with [`flash projects
      create`](/reference/cli#projects).
</Note>

Tracing records **chat completions only** (`POST /chat/completions`). The
responses, embeddings, images, and audio APIs are not recorded, and neither are
non-OpenAI-compatible native clients such as the Anthropic SDK `messages` API.

## How recording works

Freesolo exposes one managed OpenAI-compatible recording endpoint at
`https://api.freesolo.co/v1`. It is a **pass-through proxy**: it forwards your
request to the upstream provider you pick, using a provider key you supply,
returns the response unchanged, and stores a trace on the way through.
Streaming works too: frames relay unbuffered and the trace is written after
the stream completes.

Because it is a pass-through, **Freesolo stores no provider credentials and
does not bill the inference**. You send your own provider key per request and are
billed by that provider directly, separate from
[Flash billing](/reference/cost-model). The key transits the proxy as the
upstream `Authorization` header without ever being persisted to a trace or a
log.

Supported upstream providers are `openai`, `anthropic`, `openrouter`, and
`google` (Anthropic and Google through their OpenAI-compatibility layers).

### Request headers

| Header                                     | Required            | Purpose                                                                       |
| ------------------------------------------ | ------------------- | ----------------------------------------------------------------------------- |
| `Authorization: Bearer <FREESOLO_API_KEY>` | yes                 | Your Freesolo API key, authenticating the caller's org                        |
| `X-Freesolo-Provider`                      | yes                 | Upstream to forward to: `openai`, `anthropic`, `openrouter`, or `google`      |
| `X-Freesolo-Provider-Key`                  | yes                 | Your own key for that provider, forwarded upstream as `Authorization: Bearer` |
| `X-Freesolo-Project-Id`                    | yes, when recording | Project the trace is stored under                                             |
| `X-Freesolo-Record`                        | no                  | Set to `false` to proxy without recording (defaults to recording)             |

`X-Freesolo-Project-Id` takes the id (not the name) of a project that already
exists in your org; recording never creates one. Omitting it on a recording
request returns `400`, and an id outside your org returns `404`.

### Secrets are redacted

Before anything is forwarded upstream or written to a trace, the proxy
replaces two kinds of values with `[redacted]`:

* **Secret-named fields** anywhere in the request or response body:
  `authorization`, `proxy-authorization`, and keys ending in `api_key`,
  `secret`, `token`, `password`, `passwd`, `credential`, `credentials`, or
  `private_key`, compared with case, underscores, and dashes ignored. The
  `token` suffix is matched in the singular, so `max_tokens` and
  `completion_tokens` pass through, and JSON Schema definitions (a tool
  property literally named `token`) are preserved.
* **Your credentials by value**: the Freesolo API key and the provider key,
  wherever they appear, including inside message content.

Everything else (`model`, `messages`, tools, `usage`) passes through
untouched.

## Record from your app

Apply exactly one of these; both record identically through the same endpoint.

### Method A: the drop-in SDK

For Python or TypeScript apps, the `freesolo` SDK ships a drop-in that
subclasses the official OpenAI client and points itself at the recording
endpoint. Do not set the base URL yourself.

<CodeGroup>
  ```python Python theme={null}
  # pip install 'freesolo>=0.2.60'
  import os

  from freesolo import OpenAI  # AsyncOpenAI for async apps

  client = OpenAI(
      project_id="<your-project-id>",
      default_headers={
          "X-Freesolo-Provider": "openai",
          "X-Freesolo-Provider-Key": os.environ["OPENAI_API_KEY"],
      },
  )

  # unchanged from here on
  response = client.chat.completions.create(
      model="gpt-4.1-mini",
      messages=[{"role": "user", "content": "Refund my last order."}],
  )
  ```

  ```typescript TypeScript theme={null}
  // bun add '@freesolo/sdk@^0.2.60'
  import OpenAI from "@freesolo/sdk/openai";

  const client = new OpenAI({
    freesolo: { projectId: "<your-project-id>" },
    defaultHeaders: {
      "X-Freesolo-Provider": "openai",
      "X-Freesolo-Provider-Key": process.env.OPENAI_API_KEY!,
    },
  });

  const response = await client.chat.completions.create({
    model: "gpt-4.1-mini",
    messages: [{ role: "user", content: "Refund my last order." }],
  });
  ```
</CodeGroup>

The drop-in reads your Freesolo key from `FREESOLO_API_KEY` and accepts the
project id as `project_id` / `freesolo.projectId` (or `FREESOLO_PROJECT_ID`).
Carry over the original client's behavioral options (`timeout`, retries,
default headers) but **not** `base_url` or `api_key`: the drop-in owns routing
and authentication. The provider headers are not set by the SDK itself; pass
them as default headers as shown, or the proxy rejects the call with a `400`
naming the missing header.

### Method B: the base-URL proxy

Any OpenAI-compatible client works (the stock OpenAI SDK, OpenRouter, LiteLLM,
the Vercel AI SDK) with no new dependency. Change only the base URL and
headers:

```python theme={null}
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.freesolo.co/v1",
    api_key=os.environ["FREESOLO_API_KEY"],
    default_headers={
        "X-Freesolo-Project-Id": "<your-project-id>",
        "X-Freesolo-Provider": "openai",
        "X-Freesolo-Provider-Key": os.environ["OPENAI_API_KEY"],
    },
)
```

Keep every request argument, including `model` and `messages`, unchanged.

<Tip>
  Route through the recording endpoint only when `FREESOLO_API_KEY` is present,
  so your app still works when Freesolo is not configured. If a shared client
  also serves unrelated calls, build a separate client at the call site you want
  recorded instead of rerouting the shared one.
</Tip>

## Export traces as a dataset

```bash theme={null}
flash traces export --project <project-id>
```

The export reads the newest 1000 traces in the project and writes them as
`{"input", "output"}` records to `dataset/train.jsonl`, exactly the shape
[`flash env setup`](/guides/environments/overview#scaffold-one) scaffolds, so
it drops into an environment unchanged:

```jsonl dataset/train.jsonl theme={null}
{"input":"Refund my last order.","output":"Your last order has been refunded."}
```

`--format prompts` exports `{"input"}`-only rows for
[GRPO and OPD](/guides/training#choose-a-training-algorithm), which train from
prompts alone, and `--format raw` writes the stored trace rows unconverted.
See the [CLI reference](/reference/cli#traces) for the full flag list.
`flash traces export` ships in Flash 1.0.19; `--format` needs 1.0.22.

### How a trace becomes a record

| Record key | Comes from                                                                                                                                                                                         |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input`    | The request messages, always as a string. A lone user turn exports its bare text; anything longer flattens to role-prefixed lines, so a system prompt or earlier turn is kept rather than dropped. |
| `output`   | The assistant reply. A tool call is preserved whole as `{"messages": [...]}`; a multimodal reply flattens to its text blocks.                                                                      |

Traces with no extractable request/response pair are skipped, and the CLI
reports the count. Because the export is an ordinary dataset, everything in
[Datasets](/guides/datasets) applies: a scalar `output` becomes one assistant
message, and `{"messages": [...]}` a full gold trajectory for SFT.

<Warning>
  Rows whose `output` is a tool call score `0.0` under the scaffolded
  `score_response`, a string containment check that a correct tool call never
  satisfies. Train those rows with **SFT**, which needs no reward function, or
  replace the GRPO reward with one that compares the parsed tool call's name and
  arguments. Text rows are ready for GRPO as scaffolded.
</Warning>

### Seed a new environment from traces

You do not have to export by hand and wire the file up yourself.
[`flash env setup`](/reference/cli#environments) can seed a new environment
straight from a project's traces:

```bash theme={null}
flash env setup --project <project-uuid>
```

When the selected project has traces and no `dataset/train.jsonl` exists yet,
an interactive run offers to seed the dataset from them and sizes
`max_examples` to the number of rows it exported. Traces are a head start,
never a requirement: decline, or hit an export failure, and setup falls back
to the starter dataset.

## View traces in the dashboard

Each project has a **Traces** tab in the [dashboard](/platform), with a
per-trace detail view and aggregates over the project: score, pass rate, p50
latency, average output tokens and LLM calls per trace, and error rate.

The tab can also export: select the traces you want and download them as
JSONL, in the same `records`, `prompts`, and `raw` shapes as the CLI's
`--format`. The two files are interchangeable; the difference is that the
dashboard exports exactly what you select, while `flash traces export` takes
the newest 1000 in the project.

## Next steps

<CardGroup cols={2}>
  <Card title="Datasets" icon="table" href="/guides/datasets">
    Everything the exported records can express.
  </Card>

  <Card title="Training" icon="dumbbell" href="/guides/training">
    Train SFT, GRPO, or OPD on the exported dataset.
  </Card>
</CardGroup>
