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

# Wrap your LLM client

> ff.wrap() proxies your OpenAI or Anthropic client so every call records to Supabase. Tag calls with userId and a conversation id; bring your own OpenAI-compatible endpoint.

`ff.wrap(client)` returns a transparent proxy of your LLM client. You call it exactly as before; behind each `messages.create` (Anthropic), `chat.completions.create` (OpenAI), or `embeddings.create` it captures tokens, cost, latency, HTTP status, and (opt-in) prompt/completion content, then records them to your Supabase.

```ts theme={null}
const ff = new Firstflow({ persistence, captureContent: true });
const ai = ff.wrap(new Anthropic());      // or ff.wrap(new OpenAI())
```

Provider is detected by the client's shape, so there is no hard dependency on either SDK. **You build the client and supply its API key** the provider SDK's default variable (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`), your own env name passed explicitly, or `createAIClient({ apiKey })`. FirstFlow reads no credentials; it only observes the calls.

## The `firstflow` call metadata

Add a `firstflow` field to any wrapped call. It is read and **stripped** before the request reaches the provider.

```ts theme={null}
await ai.messages.create({
  model: "claude-haiku-4-5",
  messages: [...],
  firstflow: {
    userId,                 // recorded on the call/trace
    sessionId,              // alias for conversationId links into a conversation
    metadata: { route: "/api/chat" },  // merged into the row's metadata
    traceId,                // optional: correlate with an explicit trace
  },
} as any);
```

<ParamField path="userId" type="string">
  The end user the call belongs to. Stored on `firstflow_llm_calls`.
</ParamField>

<ParamField path="conversationId" type="string">
  Groups the call into a conversation. When set with `userId`, the wrap also
  records the prompting user message and the assistant reply to
  `firstflow_conversation_messages`.
</ParamField>

<ParamField path="sessionId" type="string">
  Alias for `conversationId` pass whichever name your code already uses (the
  browser SDK calls this shared id `sessionId`). When both are set,
  `conversationId` wins.
</ParamField>

<ParamField path="metadata" type="Record<string, unknown>">
  Arbitrary tags merged into the recorded row's `metadata`.
</ParamField>

## Scope: user vs. conversation

What you pass decides the scope of what's recorded:

| You pass                                | Scope                   | Recorded                                                         |
| --------------------------------------- | ----------------------- | ---------------------------------------------------------------- |
| `userId` only                           | **user**                | a standalone `firstflow_llm_calls` row                           |
| `userId` + `sessionId`/`conversationId` | **conversation + user** | the same, plus user/assistant messages grouped into a transcript |

## Bring your own OpenAI-compatible endpoint

`@firstflow/runtime-server/aiclient` is a pre-wrapped client for any OpenAI-compatible server (Ollama, vLLM, LM Studio, LocalAI, a gateway):

```ts theme={null}
import { createAIClient } from "@firstflow/runtime-server/aiclient";

const ai = createAIClient({
  baseURL: process.env.LLM_BASE_URL,        // e.g. http://localhost:11434/v1
  apiKey: process.env.LLM_API_KEY ?? "not-needed",
  persistence,
  captureContent: true,
});

await ai.chat.completions.create({
  model: "llama3.1",
  messages: [...],
  firstflow: { userId, sessionId },
} as any);
```

`openai` is an optional peer dependency loaded only by this subpath. Install it (`pnpm add openai`) when you use `createAIClient`; Anthropic-only apps never pull it in. Equivalent to `new Firstflow({ persistence }).wrap(new OpenAI({ baseURL, apiKey }))`.

## Content capture

`captureContent: true` on `new Firstflow({...})` stores prompt and completion text in `firstflow_conversation_messages`. Leave it off (the default) to record only metadata tokens, cost, latency, status.

## Next

Group multiple calls with [traces](/server/traces), see exactly [what gets recorded](/server/observe), or classify the conversation in [Intent & sentiment](/server/analytics).
