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

# Recording Completions

> Record LLM interactions to Freeplay for observability and evaluation.

Record LLM interactions to the Freeplay server for observability and evaluation. All methods associated with recordings are accessible via the `client.recordings` namespace.

## Methods Overview

| Method Name | Parameters                                | Returns                                   | Description                                                                                                |
| ----------- | ----------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `create`    | RecordPayload                             | RecordResponse with the new completion ID | Log your LLM interaction                                                                                   |
| `update`    | `completion_id:` string, `feedback:` dict | —                                         | Allows users to log additional feedback or information to a completion after it has already been recorded. |

## Record an LLM Interaction

Log your LLM interaction with Freeplay. This is assuming your have already retrieved a formatted prompt and made an LLM call as demonstrated in the [Prompts Section](/freeplay-sdk/prompts)

<CodeGroup>
  ```python python theme={null}
  from freeplay import Freeplay, RecordPayload, CallInfo, SessionInfo, CallInfo, UsageTokens

  ## PROMPT FETCH
  # set the prompt variables
  prompt_vars = {"keyA": "valueA"}

  # get a formatted prompt
  formatted_prompt = fp_client.prompts.get_formatted(
    project_id=project_id,
    template_name="template_name",
    environment="latest",
    variables=prompt_vars
  )

  ## LLM CALL
  # Make an LLM call to your provider of choice
  start = time.time()
  chat_response = openai_client.chat.completions.create(
    model=formatted_prompt.prompt_info.model,
    messages=formatted_prompt.llm_prompt,
    **formatted_prompt.prompt_info.model_parameters
  )
  end = time.time()

  # add the response to your message set
  all_messages = formatted_prompt.all_messages({
    'role': chat_response.choices[0].message.role,
    'content': chat_response.choices[0].message.content
  })

  ## RECORD
  # create a session
  session = fp_client.sessions.create()

  # build the record payload
  payload = RecordPayload(
    project_id=project_id,
    all_messages=all_messages,
    inputs=prompt_vars,
    session_info=session.session_info,
    prompt_version_info=formatted_prompt.prompt_info,
    call_info=CallInfo.from_prompt_info(
        formatted_prompt.prompt_info,
        start_time=start,
        end_time=end,
        usage=UsageTokens(
            prompt_tokens=chat_response.usage.prompt_tokens,
            completion_tokens=chat_response.usage.completion_tokens
        )
    )
  )

  # record the LLM interaction
  fp_client.recordings.create(payload)
  ```

  ```typescript typescript theme={null}
  import Freeplay, { getCallInfo, getSessionInfo } from "freeplay";
  import OpenAI from "openai";

  /* PROMPT FETCH */
  // set the prompt variables
  let promptVars = {"pop_star": "Taylor Swift"};
  // fetch a formatted prompt template
  let formattedPrompt = await fpClient.prompts.getFormatted({
      projectId,
      templateName: "template_name",
      environment: "latest",
      variables: promptVars,
  });

  /* LLM CALL */
  // make the llm call
  const openai = new OpenAI(process.env["OPENAI_API_KEY"]);

  let start = new Date();
  const chatCompletion = await openai.chat.completions.create({
      messages: formattedPrompt.llmPrompt,
      model: formattedPrompt.promptInfo.model,
    	...formattedPrompt.promptInfo.modelParameters
  });
  let end = new Date();
  console.log(chatCompletion.choices[0].message);

  // update the messages
  let messages = formattedPrompt.allMessages({
      role: chatCompletion.choices[0].message.role,
      content: chatCompletion.choices[0].message.content,
  });

  /* RECORD */
  // create a session
  let session = fpClient.sessions.create({});

  // record the LLM interaction with Freeplay
  await fpClient.recordings.create({
      projectId,
      allMessages: messages,
      inputs: promptVars,
      sessionInfo: getSessionInfo(session),
      promptVersionInfo: formattedPrompt.promptInfo,
      callInfo: getCallInfo(formattedPrompt.promptInfo, start, end),
  });
  ```

  ```java java theme={null}
  package ai.freeplay.example.java;

  import ai.freeplay.client.thin.Freeplay;
  import ai.freeplay.client.thin.resources.prompts.FormattedPrompt;
  import ai.freeplay.client.thin.resources.recordings.*;
  import ai.freeplay.client.thin.resources.sessions.SessionInfo;
  import ai.freeplay.client.thin.resources.prompts.ChatMessage;
  import ai.freeplay.example.java.ThinExampleUtils.Tuple2;

  import com.fasterxml.jackson.core.JsonProcessingException;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;

  import java.util.Map;
  import java.util.List;
  import java.net.http.HttpResponse;
  import java.util.concurrent.CompletableFuture;

  import static java.lang.String.format;
  import static ai.freeplay.client.thin.Freeplay.Config;
  import static ai.freeplay.example.java.ThinExampleUtils.callOpenAI;

  public class FreeplayRecord {
      private static final ObjectMapper objectMapper = new ObjectMapper();

      public static void main(String[] args) {

          // set your environment variables
          String openaiApiKey = System.getenv("OPENAI_API_KEY");
          String freeplayApiKey = System.getenv("FREEPLAY_API_KEY");
          String projectId = System.getenv("FREEPLAY_PROJECT_ID");
          String customerDomain = System.getenv("FREEPLAY_CUSTOMER_NAME");
          // create your url from your customer domain
          String baseUrl = format("https://%s.freeplay.ai/api", customerDomain);

          // initialize the freeplay client
          Freeplay fpClient = new Freeplay(Config()
                  .freeplayAPIKey(freeplayApiKey)
                  .customerDomain(customerDomain)
          );

          // create the prompt variables
          Map<String, Object> promptVars = Map.of("pop_star", "J Cole");
          /* FETCH PROMPT */
          var promptFuture = fpClient.prompts().<String>getFormatted(
                  projectId,
                  "album_bot",
                  "latest",
                  promptVars
          );

          /* LLM CALL */
          // set timer to be used to measure latency
          long startTime = System.currentTimeMillis();
          var llmFuture = promptFuture.thenCompose((FormattedPrompt<String> formattedPrompt) ->
                  callOpenAI(
                    objectMapper,
                    openaiApiKey,
                    formattedPrompt.getPromptInfo().getModel(), // get the model name
                    formattedPrompt.getPromptInfo().getModelParameters(), // get the model parameters
                    formattedPrompt.getBoundMessages() // get the messages, formatted for your specified provider
                  ).thenApply((HttpResponse<String> response) ->
                              new Tuple2<>(formattedPrompt, response)
                             )
                  );

          /* RECORD TO FREEPLAY */
          var recordFuture = llmFuture.thenCompose((Tuple2<FormattedPrompt<String>, HttpResponse<String>> promptAndResponse) ->
                  recordResult(
                          fpClient,
                          promptAndResponse.first, promptVars, startTime, promptAndResponse.second
                  )
          );

          recordFuture.thenApply(recordResponse -> {
              System.out.println("Recorded call succeeded with completionId: " + recordResponse.getCompletionId());
              return null;
          }).exceptionally(exception -> {
              System.out.println("Got exception: " + exception.getMessage());
              return new RecordResponse(null);
          }).join();
      }

      public static CompletableFuture<RecordResponse> recordResult(
              Freeplay fpClient,
              FormattedPrompt<String> formattedPrompt,
              Map<String, Object> promptVars,
              long startTime,
              HttpResponse<String> response
      ) {
          JsonNode bodyNode;
          try{
              bodyNode = objectMapper.readTree(response.body());
          } catch (JsonProcessingException e){
              throw new RuntimeException("Unable to parse response body", e);
          }

          // add the returned message to the list of messages
          String role = bodyNode.path("choices").path(0).path("message").path("role").asText();
          String content = bodyNode.path("choices").path(0).path("message").path("content").asText();
          List<ChatMessage> allMessages = formattedPrompt.allMessages(
                  new ChatMessage(role, content)
          );

          // construct the call info from the formatted prompt
          CallInfo callInfo = CallInfo.from(
            formattedPrompt.getPromptInfo(),
            startTime,
            System.currentTimeMillis()
          );
          // construct the session info from the session object
          SessionInfo sessionInfo = fpClient.sessions().create().getSessionInfo();

          System.out.println("Completion: " + content);

          // make the record call to freeplay
          return fpClient.recordings().create(
            new RecordInfo(
              projectId,
              allMessages
            ).inputs(variables)
            .sessionInfo(sessionInfo)
            .promptVersionInfo(formattedPrompt.getPromptInfo())
            .callInfo(callInfo)
          );
      }
  }
  }
  ```

  ```kotlin Kotlin theme={null}
  package ai.freeplay.example.kotlin

  import ai.freeplay.client.thin.Freeplay
  import ai.freeplay.client.thin.resources.prompts.ChatMessage
  import ai.freeplay.client.thin.resources.recordings.CallInfo
  import ai.freeplay.client.thin.resources.recordings.RecordInfo
  import ai.freeplay.example.java.ThinExampleUtils.callOpenAI
  import com.fasterxml.jackson.databind.ObjectMapper
  import kotlinx.coroutines.future.await
  import kotlinx.coroutines.runBlocking

  private val objectMapper = ObjectMapper()

  fun main(): Unit = runBlocking {
      // set your environment variables
      val openaiApiKey = System.getenv("OPENAI_API_KEY")
      val freeplayApiKey = System.getenv("FREEPLAY_API_KEY")
      val projectId = System.getenv("FREEPLAY_PROJECT_ID")
      val customerDomain = System.getenv("FREEPLAY_CUSTOMER_NAME")

      // create your url from your customer domain
      val baseUrl = String.format("https://%s.freeplay.ai/api", customerDomain)

      // create the freeplay client
      val fpClient = Freeplay(
              Freeplay.Config()
                      .freeplayAPIKey(freeplayApiKey)
                      .customerDomain(customerDomain)
      )

      // set the prompt variables
      val promptVars = mapOf("keyA" to "valueA")

      /* PROMPT FETCH */
      val formattedPrompt = fpClient.prompts().getFormatted<String>(
              projectId,
              "template-name",
              "latest",
              promptVars
      ).await()

      /* LLM CALL */
      // set timer to measure latency
      val startTime = System.currentTimeMillis()
      val llmResponse = callOpenAI(
              objectMapper,
              openaiApiKey,
              formattedPrompt.promptInfo.model, // get the model name
              formattedPrompt.promptInfo.modelParameters, // get the model params
              formattedPrompt.getBoundMessages()
      ).await()
      val endTime = System.currentTimeMillis()

      // add the LLM response to the message set
      val bodyNode = objectMapper.readTree(llmResponse.body())
      val role = bodyNode.path("choices").path(0).path("message").path("role").asText()
      val content = bodyNode.path("choices").path(0).path("message").path("content").asText()
      println("Completion: " + content)

      val allMessage: List<ChatMessage> = formattedPrompt.allMessages(
              ChatMessage(role, content)
      )

      /* RECORD */
      // construct the call info from the prompt object
      val callInfo = CallInfo.from(
              formattedPrompt.getPromptInfo(),
              startTime,
              endTime
      )

      // create the session and get the session info
      val sessionInfo = fpClient.sessions().create().sessionInfo

      // build the final record payload
      val recordResponse = fpClient.recordings().create(
          RecordInfo(
              projectId,
              allMessages
          ).inputs(variables)
              .sessionInfo(session.sessionInfo)
              .promptVersionInfo(prompt.promptInfo)
              .callInfo(callInfo)
              .traceInfo(trace)
      ).await()
      println("Completion Record Succeeded with ${recordResponse.completionId}")

  }
  ```
</CodeGroup>

### Using OpenAI or Anthropic's Batch APIs

Some LLM providers offer a batch method of generating completions. If you are using the Batch API, you can log your results to Freeplay with `api_style="batch"`. This parameter is needed to calculate accurate costs for batch API usage, which are often significantly lower than regular completions. The general flow for logging batch data looks like:

1. Create batch file with Freeplay completion tracking For each input, format your prompt using Freeplay's template, then create a completion record with `api_style='batch'` in the CallInfo. Store the returned `completion_id` to update the completion in Freeplay once the batch request has completed.
2. Submit batch to OpenAI and poll for completion Upload your batch file to OpenAI, create the batch request, and poll until the batch status is "completed".
3. Update Freeplay with results Once complete, read the batch output file and update each Freeplay completion using the `completion_id` from the response to match it back to the original request.

[Here](/developer-resources/recipes/openai-batch-api) is a full code example for reference.

*Note: Only use the batch api\_style if you are calling a batch LLM API. Setting this parameter incorrectly may result in incorrect costs displaying in Freeplay!*

<CodeGroup>
  ```python python theme={null}
  CallInfo(provider="openai", model="gpt-4o-mini",
      usage=UsageTokens(prompt_tokens=123, completion_tokens=456), api_style='batch'
  )
  ```
</CodeGroup>

## Recording Tools

You can record tool calls and their associated schemas for both OpenAI and Anthropic. These recorded completions and tool schemas can be viewed in the observability tab.

<Note>
  When using function calling or tool use, pass the `tool_schema` parameter in your `RecordPayload`.
  This should be a list of tool/function definitions that were available to the model. The schema can be retrieved from
  `formatted_prompt.tool_schema` if defined in your prompt template, or you can pass your own tool definitions in the same
  format you use to pass them to the model.
</Note>

The example below shows the **default approach**: tool calls are recorded as part of the completion messages. When you call `formatted_prompt.all_messages()`, the LLM's tool call output and subsequent tool results are concatenated into the message history alongside other messages.

<Tip>
  For more granular tool observability, you can also create **explicit tool spans** using traces with `kind='tool'`. This renders tool arguments and results as separate spans in the trace view, which is useful for debugging complex agent workflows. See [Tool Calls](/practical-guides/tools#logging-tool-calls-to-freeplay) for both approaches.
</Tip>

<CodeGroup>
  ```python python theme={null}
  from freeplay import Freeplay, RecordPayload, CallInfo
  from openai import OpenAI

  ## FETCH PROMPT
  # get your formatted prompt from freeplay including any associated tool schemas
  question = "What is the latest AI news?"
  prompt_vars = {"question": question}
  formatted_prompt = fp_client.prompts.get_formatted(
    project_id=project_id,
    template_name="NewsSummarizerFuncEnabled",
    variables=prompt_vars,
    environment="latest"
  )

  ## LLM CALL

  # make your llm call with your tool schemas passed in

  s = time.time()
  openai_client = OpenAI(api_key=openai_key)
  chat_response = openai_client.chat.completions.create(
    model=formatted_prompt.prompt_info.model,
    messages=formatted_prompt.llm_prompt,
    **formatted_prompt.prompt_info.model_parameters,
    tools=formatted_prompt.tool_schema
  )
  e = time.time()

  ## RECORD
  # create a session
  session = fp_client.sessions.create()

  # Append the response to the messages

  messages = formatted_prompt.all_messages(chat_response.choices[0].message)

  # Optionally prep Freeplay parameters

  # Provide token use

  token_usage = UsageTokens(
    prompt_tokens=chat_response.usage.prompt_tokens,
    completion_tokens=chat_response.usage.completion_tokens
  )

  # Provide timing information

  call_info = CallInfo.from_prompt_info(
    prompt_info=formatted_prompt.prompt_info,
    start_time=start,
    end_time=end,
    usage=usage
  )

  ## RECORD

  # Record to Freeplay
  record_response = fpclient.recordings.create(
    RecordPayload(
        project_id=project_id,
        all_messages=messages,
        session_info=session.session_info,
        inputs=prompt_vars,
        prompt_version_info=formatted_prompt.prompt_info,
        call_info=call_info,
        tool_schema=formatted_prompt.tool_schema  # Optionally record the tool schema as well
    )
  )

  ```

  ```typescript typescript theme={null}
  import Freeplay, { getCallInfo, getSessionInfo } from "freeplay";

  /* FETCH PROMPT */
  // set the prompt variables
  let promptVars = {"question": "What is the latest AI news?"};
  // fetch a formatted prompt template
  let formattedPrompt = await fpClient.prompts.getFormatted({
      projectId,
      templateName: "NewsSummarizerFuncEnabled",
      environment: "latest",
      variables: promptVars,
  });

  /* LLM CALL */
  const openai = new OpenAI(process.env["OPENAI_API_KEY"]);

  let start = new Date();
  const chatCompletion = await openai.chat.completions.create({
      messages: formattedPrompt.llmPrompt,
      model: formattedPrompt.promptInfo.model,
      tools: formattedPrompt.tool_schema,
    	...formattedPrompt.promptInfo.modelParameters
  });
  let end = new Date();
  console.log(chatCompletion.choices[0]);

  /* RECORD */
  // create the session
  let session = fpClient.sessions.create({});

  // Append the response to the messages
  let messages = formattedPrompt.allMessages(chatCompletion.choices[0].message);

  fpClient.recordings.create({
      projectId,
      // Last message captures the tool call if it exists
      allMessages: messages,
      sessionInfo: session,
      inputs: promptVars,
      promptVersionInfo: formattedPrompt.promptInfo,
      callInfo: CallInfo.fromPromptInfo(formattedPrompt.promptInfo, start, end),
      // Records the associated tool schema
      toolSchema: formattedPrompt.toolSchema
  });
  ```

  ```kotlin kotlin theme={null}
  import ai.freeplay.client.thin.Freeplay
  import ai.freeplay.client.thin.resources.prompts.ChatMessage
  import ai.freeplay.client.thin.resources.recordings.CallInfo
  import ai.freeplay.client.thin.resources.recordings.RecordInfo

  object OpenAIToolsExample {
      @JvmStatic
      fun main(args: Array<String>) {
          /* FETCH PROMPT */
          // set the prompt variables
          val variables = mapOf("question" to "What is the latest AI news?")
          fpClient.prompts()
              .getFormatted<List<ChatMessage>>(
                  projectId,
                  "NewsSummarizerFuncEnabled",
                  "latest",
                  variables,
                  null
              ).thenCompose { formattedPrompt ->
                  /* LLM CALL */
                  val startTime = System.currentTimeMillis()
                  callOpenAIWithTools(
                      openaiApiKey,
                      formattedPrompt.promptInfo.model,
                      formattedPrompt.promptInfo.modelParameters,
                      formattedPrompt.formattedPrompt,
                      formattedPrompt.toolSchema
                  ).thenApply { response ->
                      Triple(formattedPrompt, response, startTime)
                  }
              }.thenCompose { (formattedPrompt, bodyNody, startTime) ->
                  // messageNode is nested Object which is Map<String, Object>
                  val messageNode = bodyNode["choices"][0]["message"]

                  // Append the response to the messages
                  val allMessages = formattedPrompt.allMessages(message).toMutableList()

                  val callInfo = CallInfo.from(
                      formattedPrompt.promptInfo,
                      startTime,
                      System.currentTimeMillis()
                  )

                  /* RECORD */
                  // create the session
                  val sessionInfo = fpClient.sessions().create().sessionInfo

                  fpClient.recordings().create(
                    RecordInfo(
                          projectId,
                          allMessages,
                      ).inputs(variables)
                        .sessionInfo(session.sessionInfo)
                        .promptVersionInfo(prompt.promptInfo)
                        .callInfo(callInfo)
                        .traceInfo(trace).toolSchema(formattedPrompt.toolSchema)
                  ).await()
              }
              .join()
      }
  }
  ```
</CodeGroup>

### Updating a Completion

Freeplay allows you to update a completion once it has already been recorded. This can be useful to add client evals or additional messages to the completion. To do this, you will need to have the `project_id` and `completion_id`. The completion\_id is returned via `recordings.create`. The code example below shows this in action:

<CodeGroup>
  ```python python theme={null}
  #############################################################################
  # UPDATE THE COMPLETION - Use the record_response to get the completion_id
  #############################################################################
  from freeplay.resources.recordings import RecordUpdatePayload

  # Update the completion with customer feedback

  # This uses the update method to attach feedback to a specific completion

  fp_client.recordings.update(
    RecordUpdatePayload(
      project_id=PROJECT_ID,
      completion_id=final_completion_id,
      eval_results={
        "accuracy": accuracy,
        "precision": precision,
        "recall": recall,
        "f1": f1,
      },
    )
  )

  ```
</CodeGroup>

## Calling Any Model

Freeplay allows you to record LLM interactions from any model or provider, including hosts or formats Freeplay doesn't natively support. See [Calling Any Model](/freeplay-sdk/calling-any-model) for full examples across all supported languages.

## Custom Model Parameters

You can record additional model parameters beyond the defaults configured in the Freeplay UI. See [Custom Model Parameters](/freeplay-sdk/custom-model-parameters) for full examples.
