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

# Getting started with Google Gemini

This guide shows how to connect a Google Gemini-powered application to LaunchDarkly AgentControl. Gemini offers native multimodal capabilities, long context windows, and competitive pricing with Gemini Flash. By the end, you will be able to manage your model configuration and prompts outside of your application code, and track metrics automatically.

AgentControl supports two modes:

* **Completion mode** returns messages and roles (system, user, assistant). Use it for chat-style interactions and message-oriented workflows. Completion mode supports online evaluations with judges attached in the LaunchDarkly UI.
* **Agent mode** returns a single `instructions` string. Use it when your runtime or framework expects a goal/instructions input for a structured workflow. Agent mode changes the configuration shape from messages to instructions. Your application maps these instructions into your provider or framework's native input.

Both modes support tool calling. This guide walks through completion mode as the main path, with an optional agent config section. To learn more about when to use each mode, read [When to use completion mode vs agent mode](/docs/guides/agentcontrol/agent-vs-completion).

This guide provides examples in both Python and Node.js (TypeScript).

<Tip>
  **New to AgentControl?**

  If you're new to AgentControl, start with the [Quickstart](/docs/home/agentcontrol/quickstart) and return to this guide when you are ready for a more detailed example.

  To learn more about AgentControl-specific SDKs, read [AI SDKs](/docs/sdk/ai). For Python-specific details, read the [Python AI SDK reference](/docs/sdk/ai/python).
</Tip>

## Prerequisites

To complete this guide, you need the following:

* A [LaunchDarkly account](https://app.launchdarkly.com/) with an [SDK key](/docs/home/account/environment/keys#view-or-copy-sdk-credentials) for your environment and a member role that allows [AgentControl actions](/docs/home/account/roles/role-actions#agentcontrol-config-actions). To learn more about LaunchDarkly roles, read [Roles](/docs/home/account/roles).
* A Google API key with Gemini API access. Create one at [aistudio.google.com](https://aistudio.google.com/).
* A development environment:
  * **Python**: Python 3.10 or higher
  * **Node.js**: Node.js 20 or higher
* Familiarity with LaunchDarkly contexts. To learn more, read [Contexts and segments](/docs/home/flags/contexts).

## Concepts

Before you begin, review these key concepts.

### AgentControl configs

An AgentControl config is a LaunchDarkly resource that controls how your application uses large language models. Each AgentControl config contains one or more variations. Each variation specifies:

* A model configuration, including the model name and parameters
* Messages that define the prompt

You can update these settings in LaunchDarkly at any time without changing your application code.

### Contexts

A context represents the end user interacting with your application. LaunchDarkly uses context attributes to:

* Determine which variation to serve based on targeting rules
* Populate `{{ ldctx.* }}` placeholders in your prompts with context attribute values

Other placeholders, such as `{{ topic }}`, are populated from the `variables` argument you pass at runtime.

### The tracker

When you retrieve an AgentControl config, call `createTracker()` (Node.js) or `create_tracker()` (Python) on the returned object to mint a tracker. The tracker records metrics from your Gemini calls, including:

* Generation count
* Input and output tokens
* Latency
* Success and error rates

These metrics appear on the **AI Insights** dashboard in LaunchDarkly.

## Step 1: Install the SDK

Install the LaunchDarkly AI SDK and the Google GenAI SDK in your application. AgentControl is supported by LaunchDarkly server-side SDKs only. The Node.js examples in this guide use the [server-side Node.js AI SDK](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/server-ai).

Here is how to install the required packages:

<CodeGroup>
  ```bash title="Python" lines wrap theme={null}
  pip install "launchdarkly-server-sdk-ai>=0.20.0"
  pip install google-genai
  pip install python-dotenv
  ```

  ```bash title="Node.js" lines wrap theme={null}
  npm install @launchdarkly/node-server-sdk
  npm install "@launchdarkly/server-sdk-ai@^0.20.0"
  npm install @google/genai
  npm install dotenv
  ```
</CodeGroup>

Create a `.env` file in your project root to store your API keys:

```bash lines wrap theme={null}
# .env
LAUNCHDARKLY_SDK_KEY=<your-launchdarkly-sdk-key>
GEMINI_API_KEY=<your-gemini-api-key>
```

Add `.env` to your `.gitignore` to keep credentials out of version control.

## Step 2: Initialize the clients

Initialize both the LaunchDarkly client and the Google GenAI client. Store your API keys in environment variables.

Here is the initialization code:

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  import os
  from typing import List, Optional, Tuple
  import ldclient
  from ldclient import Context
  from ldclient.config import Config
  from ldai import LDAIClient, LDMessage
  from ldai.providers.types import LDAIMetrics
  from ldai.tracker import TokenUsage
  from google import genai
  from google.genai import types
  from dotenv import load_dotenv

  load_dotenv()

  GOOGLE_API_KEY = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
  SDK_KEY = os.environ.get("LAUNCHDARKLY_SDK_KEY_GEMINI")
  CONFIG_KEY = "gemini-assistant"
  AGENT_CONFIG_KEY = "gemini-agent"

  gemini_client = genai.Client(api_key=GOOGLE_API_KEY)
  ldclient.set_config(Config(SDK_KEY))
  if not ldclient.get().is_initialized():
      exit(1)

  ai_client = LDAIClient(ldclient.get())
  ```

  ```typescript title="Node.js (TypeScript)" expandable lines wrap theme={null}
  import dotenv from 'dotenv';
  dotenv.config();
  import { init, LDClient, LDContext } from '@launchdarkly/node-server-sdk';
  import {
    initAi,
    type LDAIClient,
    type LDAICompletionConfig,
    type LDAIAgentConfig,
    type LDAIMetrics,
  } from '@launchdarkly/server-sdk-ai';
  import { GoogleGenAI, type Content } from '@google/genai';

  const GOOGLE_API_KEY = (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) as string;
  const SDK_KEY = process.env.LAUNCHDARKLY_SDK_KEY_GEMINI as string;
  const CONFIG_KEY = 'gemini-assistant';
  const AGENT_CONFIG_KEY = 'gemini-agent';

  const genAI = new GoogleGenAI({ apiKey: GOOGLE_API_KEY });
  const ldClient: LDClient = init(SDK_KEY);
  await ldClient.waitForInitialization({ timeout: 10 });

  const aiClient: LDAIClient = initAi(ldClient);
  ```
</CodeGroup>

## Step 3: Create an AgentControl config in LaunchDarkly

Create an AgentControl config in the LaunchDarkly UI to store your Gemini model settings and prompts.

<Tip>
  **Using the MCP server or agent skills**

  If you have the [LaunchDarkly MCP server](https://github.com/launchdarkly/mcp-server) or [agent skills](https://github.com/launchdarkly/agent-skills) configured, prompt your coding assistant to create the AgentControl config for you. For example:

  "Create a completion mode AgentControl config called 'Gemini assistant' with a 'Gemini 2.0 Flash' variation using the gemini-2.0-flash model, temperature 0.7, max\_tokens 1024, and the system message: 'You are a helpful assistant. Answer questions about \{\{topic}}.' Enable targeting."
</Tip>

To create the AgentControl config:

1. In the left sidebar, click **Create** and select **Config**.
2. In the "Create config" dialog, select **Completion**.
3. Enter a name, such as "Gemini assistant".
4. Click **Create**.

<Frame caption="The &#x22;Create&#x22; menu options.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/create-menu.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=5b3fe40c4dd161b2972de2334ff19c0a" alt="The &#x22;Create&#x22; menu options." width="1180" height="440" data-path="images/auto/create-menu.auto.png" />
</Frame>

To create a variation:

1. On the **Variations** tab, replace "Untitled variation" with a name, such as "Gemini 2.0 Flash".
2. Click **Select a model** and choose the `gemini-2.0-flash` Gemini model.
3. Click **Parameters** and set `temperature` to `0.7` and `max_tokens` to `1024`.
4. Add a **system** message to define your assistant's behavior:

<CodeGroup>
  ```text title="System message" lines wrap theme={null}
  You are a helpful assistant. Answer questions about {{topic}}.
  ```
</CodeGroup>

5. Click **Review and save**.

<Frame caption="A completed variation with model configuration and system message.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/ai-config-variation-complete.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=e6b80ed68d6edb05d7f31ee72e6b4974" alt="A completed variation with model configuration and system message." width="2070" height="638" data-path="images/auto/ai-config-variation-complete.auto.png" />
</Frame>

To enable targeting:

1. Select the **Targeting** tab.
2. In the "Default rule" section, click **Edit**.
3. Set the default rule to serve your variation.
4. Click **Review and save**.

<Frame caption="The default targeting rule configured to serve a variation.">
  <img src="https://mintcdn.com/launchdarkly/hutarVphEq2dY_zb/images/auto/guide-ai-config-model-config-update-default-targeting-rule.auto.png?fit=max&auto=format&n=hutarVphEq2dY_zb&q=85&s=b92bcc5a507600be0fd944fbd203113e" alt="The default targeting rule configured to serve a variation." width="2074" height="1138" data-path="images/auto/guide-ai-config-model-config-update-default-targeting-rule.auto.png" />
</Frame>

## Step 4: Get the AgentControl config in your application

Retrieve the AgentControl config from LaunchDarkly by calling the completion config function. Pass a context that represents the current user and a fallback configuration.

Here is how to get the AgentControl config:

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  # Define the context for the current user
  context = Context.builder("user-123") \
      .kind("user") \
      .name("Sandy") \
      .build()

  # Pass a default for improved resiliency when the AgentControl config is unavailable
  # or LaunchDarkly is unreachable; omit for a disabled default.
  # Example:
  #   from ldai import AICompletionConfigDefault
  #   default = AICompletionConfigDefault(
  #       enabled=True,
  #       model={"name": "gemini-2.5-flash"},
  #       provider={"name": "gemini"},
  #       messages=[{"role": "system", "content": "You are a helpful assistant."}],
  #   )
  #   config = ai_client.completion_config(CONFIG_KEY, context, default, variables={"topic": "Python"})

  # Get the AgentControl config
  config = ai_client.completion_config(CONFIG_KEY, context, variables={"topic": "Python"})
  tracker = config.create_tracker()
  ```

  ```typescript title="Node.js (TypeScript)" expandable lines wrap theme={null}
  // Define the context for the current user
  const context: LDContext = {
    kind: 'user',
    key: 'user-123',
    name: 'Sandy',
  };

  // Pass a default for improved resiliency when the AgentControl config is unavailable
  // or LaunchDarkly is unreachable; omit for a disabled default.
  // Example:
  //   const fallback = {
  //     enabled: true,
  //     model: { name: 'gemini-2.5-flash' },
  //     provider: { name: 'gemini' },
  //     messages: [{ role: 'system', content: 'You are a helpful assistant.' }],
  //   };
  //   const aiConfig = await aiClient.completionConfig(CONFIG_KEY, context, fallback, { topic: 'Python' });

  // Get the AgentControl config
  const aiConfig: LDAICompletionConfig = await aiClient.completionConfig(
    CONFIG_KEY,
    context,
    undefined,
    { topic: 'Python' },
  );
  ```
</CodeGroup>

The SDK uses the fallback configuration when LaunchDarkly is unreachable. Check the `enabled` property and handle the disabled case in your application.

<Note>
  **Best practices**

  For production use:

  * Retrieve the AgentControl config each time you generate content so LaunchDarkly can evaluate the latest targeting rules and prompt changes.
  * Provide a fallback configuration when possible so your application can fail gracefully if LaunchDarkly is unavailable.
  * Avoid sending personally identifiable information in contexts unless you have a specific need and an approved handling pattern. To learn more, read [Privacy in AgentControl](/docs/home/agentcontrol/privacy).
</Note>

## Step 5: Call Gemini and track metrics

Google's GenAI SDK treats `system_instruction` as a top-level config field on `GenerateContentConfig`, separate from the contents array. Define a helper to split LaunchDarkly messages into a `(system_instruction, contents)` tuple: system messages are concatenated into a single string, `user` messages become `role: "user"` content items, and `assistant` messages become `role: "model"` content items. Append the user's question to the contents list, then call `generate_content` directly instead of creating a chat session.

Gemini's parameter names differ slightly from the LaunchDarkly AgentControl config field names. The `max_tokens` parameter stored in a variation becomes `max_output_tokens` in the Gemini SDK. Define a mapping helper to handle this rename, then define a converter function that translates a Gemini response into an `LDAIMetrics` object. Pass the converter to the tracker's generic wrapper, which handles duration, success, and error tracking automatically:

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  def gemini_config_kwargs(params):
      """Map AgentControl config parameter names to google.genai GenerateContentConfig arguments.
      LaunchDarkly's max_tokens needs to become max_output_tokens; the rest
      (temperature, topP, topK, stopSequences) already match the SDK's snake_case aliases.
      Drop `tools` — we pass them via the GenerateContentConfig.tools field, so
      leaving them in params would duplicate the argument."""
      mapping = {"max_tokens": "max_output_tokens"}
      return {mapping.get(k, k): v for k, v in (params or {}).items() if k != "tools"}

  def map_to_google_ai_messages(
      input_messages: List[LDMessage],
  ) -> Tuple[Optional[str], List[types.Content]]:
      """Split LaunchDarkly messages into (system_instruction, history) for google.genai.
      System messages are concatenated into a top-level system_instruction;
      user/assistant go into Content history (assistant role becomes `model`)."""
      history: List[types.Content] = []
      system_messages: List[str] = []
      for m in input_messages:
          if m.role == "system":
              system_messages.append(m.content)
          elif m.role == "user":
              history.append(types.Content(role="user", parts=[types.Part(text=m.content)]))
          elif m.role == "assistant":
              history.append(types.Content(role="model", parts=[types.Part(text=m.content)]))
      system_instruction = " ".join(system_messages) if system_messages else None
      return system_instruction, history

  def gemini_metrics(response) -> LDAIMetrics:
      """Convert a google.genai response into LDAIMetrics. Passed to
      tracker.track_metrics_of, which handles duration + success/error itself."""
      usage = getattr(response, "usage_metadata", None)
      tokens = None
      if usage:
          tokens = TokenUsage(
              total=usage.total_token_count or 0,
              input=usage.prompt_token_count or 0,
              output=usage.candidates_token_count or 0,
          )
      return LDAIMetrics(success=True, tokens=tokens)
  ```

  ```typescript title="Node.js (TypeScript)" expandable lines wrap theme={null}
  // Map LaunchDarkly params to @google/genai's generationConfig keys. max_tokens → maxOutputTokens.
  // Drop `tools` — they go on GenerateContentConfig.tools, so leaving them in params would duplicate.
  function geminiConfigFields(params: Record<string, unknown>): Record<string, unknown> {
    const mapping: Record<string, string> = { max_tokens: 'maxOutputTokens' };
    return Object.fromEntries(
      Object.entries(params || {})
        .filter(([k]) => k !== 'tools')
        .map(([k, v]) => [mapping[k] || k, v])
    );
  }

  // Split LaunchDarkly messages into (systemInstruction, contents) for @google/genai.
  // System messages → top-level systemInstruction; user/assistant → Content[].
  function mapToGoogleAIMessages(
    messages: Array<{ role: string; content: string }>,
  ): { systemInstruction: string | undefined; contents: Content[] } {
    const contents: Content[] = [];
    const systemParts: string[] = [];
    for (const msg of messages) {
      if (msg.role === 'system') systemParts.push(msg.content);
      else if (msg.role === 'user') contents.push({ role: 'user', parts: [{ text: msg.content }] });
      else if (msg.role === 'assistant') contents.push({ role: 'model', parts: [{ text: msg.content }] });
    }
    return {
      systemInstruction: systemParts.length ? systemParts.join(' ') : undefined,
      contents,
    };
  }

  // Convert a @google/genai response into LDAIMetrics. Passed to tracker.trackMetricsOf.
  function geminiMetrics(response: any): LDAIMetrics {
    const usage = response.usageMetadata;
    return {
      success: true,
      tokens: usage
        ? {
            input: usage.promptTokenCount || 0,
            output: usage.candidatesTokenCount || 0,
            total: usage.totalTokenCount || 0,
          }
        : undefined,
    };
  }
  ```
</CodeGroup>

Then call Gemini with the config. Use `map_to_google_ai_messages` / `mapToGoogleAIMessages` to split the LaunchDarkly messages into a system instruction and a contents array, append the user's question, then call `generate_content` / `generateContent` directly through the tracker wrapper:

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  if config.enabled:
      tracker = config.create_tracker()
      system_instruction, contents = map_to_google_ai_messages(config.messages or [])
      contents.append(types.Content(role="user", parts=[types.Part(text="How do I read a file in Python?")]))

      ld_params = (config.model.to_dict().get("parameters") if config.model else None) or {}

      response = tracker.track_metrics_of(
          gemini_metrics,
          lambda: gemini_client.models.generate_content(
              model=config.model.name,
              contents=contents,
              config=types.GenerateContentConfig(
                  system_instruction=system_instruction,
                  **gemini_config_kwargs(ld_params),
              ),
          ),
      )
  ```

  ```typescript title="Node.js (TypeScript)" expandable lines wrap theme={null}
  if (aiConfig.enabled) {
    const tracker = aiConfig.createTracker();
    const { systemInstruction, contents } = mapToGoogleAIMessages(aiConfig.messages || []);
    contents.push({ role: 'user', parts: [{ text: 'How do I read a file in Python?' }] });

    await tracker.trackMetricsOf(geminiMetrics, () =>
      genAI.models.generateContent({
        model: aiConfig.model!.name,
        contents,
        config: {
          systemInstruction,
          ...geminiConfigFields((aiConfig.model?.parameters || {}) as Record<string, unknown>),
        },
      }),
    );
  }
  ```
</CodeGroup>

## Step 6: (optional): Use agent mode with tool calling

Agent-mode AgentControl configs return a single `instructions` string instead of a message list, and they let you attach reusable tools from the LaunchDarkly tools library. With Gemini, the instructions map to the `system_instruction` field on `GenerateContentConfig` and tools pass through as a `functionDeclarations` array.

### Create the tool in the tools library

First, define the tool in LaunchDarkly so the AgentControl config variation can reference it:

1. In the left sidebar, click **Library**, then select the **Tools** tab.
2. Click **Add tool**.
3. Enter `get_order_status` as the **Key**.
4. Enter "Look up the status of a customer order by order ID" as the **Description**.
5. Define the schema using the JSON editor:

```json lines wrap theme={null}
{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string",
      "description": "The order ID to look up"
    }
  },
  "required": ["order_id"]
}
```

6. Click **Save**.

<Frame caption="The Create tool dialog.">
  <img src="https://mintcdn.com/launchdarkly/WYiCaDy82H6mz_dK/images/auto/ai-configs-create-tool.auto.png?fit=max&auto=format&n=WYiCaDy82H6mz_dK&q=85&s=958a8759bedffa7e426d66ae51dbd998" alt="The Create tool dialog." width="1240" height="1468" data-path="images/auto/ai-configs-create-tool.auto.png" />
</Frame>

### Create the agent AgentControl config

1. Click **Create** and select **Config**.
2. Select **Agent** mode.

<Frame caption="The Create AgentControl config dialog with Agent mode selected.">
  <img src="https://mintcdn.com/launchdarkly/WYiCaDy82H6mz_dK/images/auto/agentcontrol-snippet-create-agent.auto.png?fit=max&auto=format&n=WYiCaDy82H6mz_dK&q=85&s=0132e925414611e4468858bce6392568" alt="The Create AgentControl config dialog with Agent mode selected." width="920" height="984" data-path="images/auto/agentcontrol-snippet-create-agent.auto.png" />
</Frame>

3. Enter a name:

<CodeGroup>
  ```text title="AgentControl config name" lines wrap theme={null}
  Gemini agent
  ```
</CodeGroup>

4. Click **Create**.
5. On the **Variations** tab, name the variation (for example, "Gemini 2.0 Flash agent").
6. Click **Select a model** and choose `gemini-2.0-flash`.
7. Click **Parameters** and set `max_tokens` to `1024`.
8. Add the agent **instructions**:

<CodeGroup>
  ```text title="Agent instructions" lines wrap theme={null}
  You are an order status assistant. Use the get_order_status tool to look up customer orders by ID. Only call the tool when the user asks about an order.
  ```
</CodeGroup>

9. Click **+ Attach tools** and select `get_order_status`.

<Frame caption="A variation editor with an attached tool.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/ai-configs-variations-with-tool.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=60dba16f862a59385da8b203cfcf7d21" alt="A variation editor with an attached tool." width="1980" height="868" data-path="images/auto/ai-configs-variations-with-tool.auto.png" />
</Frame>

10. Click **Review and save**.
11. On the **Targeting** tab, set the default rule to serve your variation and save.

To learn more about managing tools, read [Tools in AgentControl](/docs/home/agentcontrol/tools).

### Retrieve the agent config and run the tool loop

Use `agent_config()` instead of `completion_config()`. The SDK returns the attached tools under `parameters.tools` in OpenAI's `type=function` shape, so convert them to Gemini's `{functionDeclarations: [{name, description, parameters}]}` shape and pass them on the `tools` field of `GenerateContentConfig`. Tool handler functions stay in your application code — LaunchDarkly stores the schema, your application owns the behavior.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  # Same fallback pattern as completion — omit the default for a disabled fallback.
  agent = ai_client.agent_config(AGENT_CONFIG_KEY, context)

  if agent.enabled:
      tracker = agent.create_tracker()
      ld_params = (agent.model.to_dict().get("parameters") if agent.model else None) or {}

      # LaunchDarkly returns attached tools under parameters.tools in OpenAI type=function shape.
      # Gemini wants {functionDeclarations: [{name, description, parameters}]}.
      ld_tools = ld_params.get("tools", []) or []
      tools = [{
          "functionDeclarations": [
              {
                  "name": t["name"],
                  "description": t.get("description", ""),
                  "parameters": t.get("parameters", {"type": "object", "properties": {}}),
              }
              for t in ld_tools
          ]
      }] if ld_tools else []

      # Handlers stay in application code — LaunchDarkly governs the schema, the app owns execution.
      def get_order_status(order_id: str) -> str:
          orders = {
              "ORD-123": "Shipped — arrives Thursday",
              "ORD-456": "Processing — estimated ship date: tomorrow",
              "ORD-789": "Delivered on Monday",
          }
          return orders.get(order_id, f"No order found with ID {order_id}")

      tool_handlers = {"get_order_status": get_order_status}

      chat = gemini_client.chats.create(
          model=agent.model.name,
          config=types.GenerateContentConfig(
              system_instruction=agent.instructions,
              tools=tools,
              **gemini_config_kwargs(ld_params),
          ),
      )

      # Agent loop: send, handle functionCalls, repeat
      MAX_STEPS = 5
      message_payload: object = "What's the status of order ORD-123?"
      for _ in range(MAX_STEPS):
          response = tracker.track_metrics_of(
              gemini_metrics,
              lambda: chat.send_message(message_payload),
          )

          calls = response.function_calls or []
          if not calls:
              break

          function_response_parts = []
          for call in calls:
              if call.name not in tool_handlers:
                  raise ValueError(f"Unknown tool: {call.name}")
              result = tool_handlers[call.name](**call.args)
              tracker.track_tool_call(call.name)
              function_response_parts.append(
                  types.Part(function_response=types.FunctionResponse(name=call.name, response={"result": result}))
              )
          message_payload = function_response_parts
  ```

  ```typescript title="Node.js (TypeScript)" expandable lines wrap theme={null}
  // Same fallback pattern as completion — omit the default for a disabled fallback.
  const agentConfig: LDAIAgentConfig = await aiClient.agentConfig(AGENT_CONFIG_KEY, context);

  if (agentConfig.enabled && agentConfig.instructions) {
    const tracker = agentConfig.createTracker();
    const ldParams = (agentConfig.model?.parameters || {}) as Record<string, unknown>;

    // LaunchDarkly returns attached tools under parameters.tools in OpenAI type=function shape.
    // Gemini wants { functionDeclarations: [{ name, description, parameters }] }.
    const ldTools = (ldParams.tools as any[] | undefined) ?? [];
    const tools = ldTools.length
      ? [{
          functionDeclarations: ldTools.map((t) => ({
            name: t.name as string,
            description: (t.description as string) ?? '',
            parameters: (t.parameters as any) ?? { type: 'object', properties: {} },
          })),
        }]
      : [];

    // Handlers stay in application code — LaunchDarkly governs the schema, the app owns execution.
    function getOrderStatus(orderId: string): string {
      const orders: Record<string, string> = {
        'ORD-123': 'Shipped — arrives Thursday',
        'ORD-456': 'Processing — estimated ship date: tomorrow',
        'ORD-789': 'Delivered on Monday',
      };
      return orders[orderId] || `No order found with ID ${orderId}`;
    }

    const toolHandlers: Record<string, (args: any) => string> = {
      get_order_status: (args) => getOrderStatus(args.order_id),
    };

    const chat = genAI.chats.create({
      model: agentConfig.model!.name,
      config: {
        systemInstruction: agentConfig.instructions,
        tools,
        ...geminiConfigFields((agentConfig.model?.parameters || {}) as Record<string, unknown>),
      },
    });

    // Agent loop: send, handle functionCalls, repeat
    const MAX_STEPS = 5;
    let messagePayload: any = "What's the status of order ORD-123?";
    for (let i = 0; i < MAX_STEPS; i++) {
      const response = await tracker.trackMetricsOf(geminiMetrics, () =>
        chat.sendMessage({ message: messagePayload }),
      );

      const calls = response.functionCalls || [];
      if (calls.length === 0) {
        break;
      }

      const functionResponseParts = calls.map(call => {
        if (!toolHandlers[call.name!]) throw new Error(`Unknown tool: ${call.name}`);
        const result = toolHandlers[call.name!](call.args);
        return {
          functionResponse: { name: call.name!, response: { result } },
        };
      });
      messagePayload = functionResponseParts;
    }
  }
  ```
</CodeGroup>

In completion mode, you can attach judges to variations in the LaunchDarkly UI for automatic evaluation. In agent mode, invoke judges programmatically through the AI SDK.

To learn more, read [When to use completion mode vs agent mode](/docs/guides/agentcontrol/agent-vs-completion) and [Agents in AgentControl](/docs/home/agentcontrol/agents).

## Step 7: Monitor your AgentControl config

Use the LaunchDarkly UI to monitor how your applications are performing across all AgentControl configs and for individual configs.

To view aggregated metrics across all your AgentControl configs, navigate to **Insights** in the left sidebar under the **AI** section. The Insights overview page displays cost, latency, error rate, invocation counts, and model distribution across your organization. To learn more, read [AI Insights](/docs/home/agentcontrol/insights).

To view metrics for a specific AgentControl config:

1. Navigate to your AgentControl config.
2. Select the **Monitoring** tab.

The dashboard displays the following metrics:

* **Generation count**: The total number of AI generation calls tracked for this config.
* **Input and output tokens**: Token consumption broken down by prompt tokens sent and completion tokens received.
* **Latency**: The time taken for each generation call, shown as percentiles (p50, p95).
* **Success and error rates**: The proportion of successful versus failed generation calls.

Metrics update approximately every minute. Use these metrics to compare variations and optimize your prompts. To learn more, read [Monitor AgentControl configs](/docs/home/agentcontrol/monitor).

<Frame caption="The Insights overview page showing cost, latency, error rate, and invocation metrics for Gemini AgentControl configs.">
  <img src="https://mintcdn.com/launchdarkly/Y_qcqLWSC5ccm6eB/images/__LD_UI_no_test/guide-ai-config-gemini-insights.png?fit=max&auto=format&n=Y_qcqLWSC5ccm6eB&q=85&s=fbd88bad0d445e995e95d84d522446cf" alt="The Insights overview page showing cost, latency, error rate, and invocation metrics for Gemini AgentControl configs." width="3338" height="1639" data-path="images/__LD_UI_no_test/guide-ai-config-gemini-insights.png" />
</Frame>

<Tip>
  **Observability**

  The AI SDKs emit OpenTelemetry-compatible spans for each generation call. You can forward these spans to your existing observability stack for deeper analysis. To learn more, read [Observability](/docs/home/observability) and [LLM observability](/docs/home/observability/llm-observability).
</Tip>

## Step 8: Close the client

Close the LaunchDarkly client when your application shuts down to flush pending events.

Here is how to close the client:

<CodeGroup>
  ```python title="Python" lines wrap theme={null}
  ldclient.get().close()
  ```

  ```typescript title="Node.js (TypeScript)" lines wrap theme={null}
  await ldClient.close();
  ```
</CodeGroup>

Always flush events before closing. Trailing events are at risk of being lost otherwise, in short-lived scripts and long-running services alike.

Here is how to flush events:

<CodeGroup>
  ```python title="Python" lines wrap theme={null}
  # Always flush events before closing — trailing events are at risk of being
  # lost otherwise, in short-lived scripts and long-running services alike.
  ldclient.get().flush()
  ldclient.get().close()
  ```

  ```typescript title="Node.js (TypeScript)" lines wrap theme={null}
  // Always flush events before closing — trailing events are at risk of being
  // lost otherwise, in short-lived scripts and long-running services alike.
  await ldClient.flush();
  await ldClient.close();
  ```
</CodeGroup>

## Complete example

Here is a complete working example that combines all the steps.

<Accordion title="Click to expand full example code">
  <CodeGroup>
    ```python title="Python" expandable lines wrap theme={null}
    import os
    from typing import List, Optional, Tuple
    import ldclient
    from ldclient import Context
    from ldclient.config import Config
    from ldai import LDAIClient, LDMessage
    from ldai.providers.types import LDAIMetrics
    from ldai.tracker import TokenUsage
    from google import genai
    from google.genai import types
    from dotenv import load_dotenv

    load_dotenv()

    GOOGLE_API_KEY = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
    SDK_KEY = os.environ.get("LAUNCHDARKLY_SDK_KEY_GEMINI")
    CONFIG_KEY = "gemini-assistant"
    AGENT_CONFIG_KEY = "gemini-agent"


    def gemini_config_kwargs(params):
        """Map AgentControl config parameter names to google.genai GenerateContentConfig arguments.
        LaunchDarkly's max_tokens needs to become max_output_tokens; the rest
        (temperature, topP, topK, stopSequences) already match the SDK's snake_case aliases.
        Drop `tools` — we pass them via the GenerateContentConfig.tools field, so
        leaving them in params would duplicate the argument."""
        mapping = {"max_tokens": "max_output_tokens"}
        return {mapping.get(k, k): v for k, v in (params or {}).items() if k != "tools"}


    def map_to_google_ai_messages(
        input_messages: List[LDMessage],
    ) -> Tuple[Optional[str], List[types.Content]]:
        """Split LaunchDarkly messages into (system_instruction, history) for google.genai.
        System messages are concatenated into a top-level system_instruction;
        user/assistant go into Content history (assistant role becomes `model`)."""
        history: List[types.Content] = []
        system_messages: List[str] = []
        for m in input_messages:
            if m.role == "system":
                system_messages.append(m.content)
            elif m.role == "user":
                history.append(types.Content(role="user", parts=[types.Part(text=m.content)]))
            elif m.role == "assistant":
                history.append(types.Content(role="model", parts=[types.Part(text=m.content)]))
        system_instruction = " ".join(system_messages) if system_messages else None
        return system_instruction, history


    def gemini_metrics(response) -> LDAIMetrics:
        """Convert a google.genai response into LDAIMetrics. Passed to
        tracker.track_metrics_of, which handles duration + success/error itself."""
        usage = getattr(response, "usage_metadata", None)
        tokens = None
        if usage:
            tokens = TokenUsage(
                total=usage.total_token_count or 0,
                input=usage.prompt_token_count or 0,
                output=usage.candidates_token_count or 0,
            )
        return LDAIMetrics(success=True, tokens=tokens)


    def main():
        gemini_client = genai.Client(api_key=GOOGLE_API_KEY)
        ldclient.set_config(Config(SDK_KEY))
        if not ldclient.get().is_initialized():
            return

        ai_client = LDAIClient(ldclient.get())

        context = Context.builder("user-123").kind("user").name("Sandy").build()

        # ===================
        # COMPLETION MODE
        # ===================

        # Pass a default for improved resiliency when the AgentControl config is unavailable
        # or LaunchDarkly is unreachable; omit for a disabled default.
        # Example:
        #   from ldai import AICompletionConfigDefault
        #   default = AICompletionConfigDefault(
        #       enabled=True,
        #       model={"name": "gemini-2.5-flash"},
        #       provider={"name": "gemini"},
        #       messages=[{"role": "system", "content": "You are a helpful assistant."}],
        #   )
        #   config = ai_client.completion_config("gemini-assistant", context, default, variables={"topic": "Python"})
        config = ai_client.completion_config(CONFIG_KEY, context, variables={"topic": "Python"})

        if config.enabled:
            tracker = config.create_tracker()
            system_instruction, contents = map_to_google_ai_messages(config.messages or [])
            contents.append(types.Content(role="user", parts=[types.Part(text="How do I read a file in Python?")]))

            ld_params = (config.model.to_dict().get("parameters") if config.model else None) or {}

            tracker.track_metrics_of(
                lambda: gemini_client.models.generate_content(
                    model=config.model.name,
                    contents=contents,
                    config=types.GenerateContentConfig(
                        system_instruction=system_instruction,
                        **gemini_config_kwargs(ld_params),
                    ),
                ),
                gemini_metrics,
            )

        # ===================
        # AGENT MODE
        # ===================

        # Same fallback pattern as completion — omit the default for a disabled fallback.
        agent = ai_client.agent_config(AGENT_CONFIG_KEY, context)

        if agent.enabled:
            tracker = agent.create_tracker()
            ld_params = (agent.model.to_dict().get("parameters") if agent.model else None) or {}

            # LaunchDarkly returns attached tools under parameters.tools in OpenAI type=function shape.
            # Gemini wants {functionDeclarations: [{name, description, parameters}]}.
            ld_tools = ld_params.get("tools", []) or []
            tools = [{
                "functionDeclarations": [{
                    "name": t["name"],
                    "description": t.get("description", ""),
                    "parameters": t.get("parameters", {"type": "object", "properties": {}}),
                } for t in ld_tools]
            }] if ld_tools else []

            # Handlers stay in application code — LaunchDarkly governs the schema, the app owns execution.
            def get_order_status(order_id: str) -> str:
                orders = {
                    "ORD-123": "Shipped — arrives Thursday",
                    "ORD-456": "Processing — estimated ship date: tomorrow",
                    "ORD-789": "Delivered on Monday",
                }
                return orders.get(order_id, f"No order found with ID {order_id}")

            tool_handlers = {"get_order_status": get_order_status}

            chat = gemini_client.chats.create(
                model=agent.model.name,
                config=types.GenerateContentConfig(
                    system_instruction=agent.instructions,
                    tools=tools,
                    **gemini_config_kwargs(ld_params),
                ),
            )

            # Agent loop: send, handle functionCalls, repeat
            MAX_STEPS = 5
            message_payload: object = "What's the status of order ORD-123?"
            for _ in range(MAX_STEPS):
                response = tracker.track_metrics_of(
                    lambda: chat.send_message(message_payload),
                    gemini_metrics,
                )

                calls = response.function_calls or []
                if not calls:
                    break

                function_response_parts = []
                for call in calls:
                    if call.name not in tool_handlers:
                        raise ValueError(f"Unknown tool: {call.name}")
                    result = tool_handlers[call.name](**call.args)
                    tracker.track_tool_call(call.name)
                    function_response_parts.append(
                        types.Part(function_response=types.FunctionResponse(name=call.name, response={"result": result}))
                    )
                message_payload = function_response_parts

        # Always flush events before closing — trailing events are at risk of being
        # lost otherwise, in short-lived scripts and long-running services alike.
        ldclient.get().flush()
        ldclient.get().close()


    if __name__ == "__main__":
        main()
    ```

    ```typescript title="Node.js (TypeScript)" expandable lines wrap theme={null}
    import dotenv from 'dotenv';
    dotenv.config();
    import { init, LDClient, LDContext } from '@launchdarkly/node-server-sdk';
    import {
      initAi,
      type LDAIClient,
      type LDAICompletionConfig,
      type LDAIAgentConfig,
      type LDAIMetrics,
    } from '@launchdarkly/server-sdk-ai';
    import { GoogleGenAI, type Content } from '@google/genai';

    const GOOGLE_API_KEY = (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) as string;
    const SDK_KEY = process.env.LAUNCHDARKLY_SDK_KEY_GEMINI as string;
    const CONFIG_KEY = 'gemini-assistant';
    const AGENT_CONFIG_KEY = 'gemini-agent';

    // Map LaunchDarkly params to @google/genai's generationConfig keys. max_tokens → maxOutputTokens.
    // Drop `tools` — they go on GenerateContentConfig.tools, so leaving them in params would duplicate.
    function geminiConfigFields(params: Record<string, unknown>): Record<string, unknown> {
      const mapping: Record<string, string> = { max_tokens: 'maxOutputTokens' };
      return Object.fromEntries(
        Object.entries(params || {})
          .filter(([k]) => k !== 'tools')
          .map(([k, v]) => [mapping[k] || k, v])
      );
    }

    // Split LaunchDarkly messages into (systemInstruction, contents) for @google/genai.
    // System messages → top-level systemInstruction; user/assistant → Content[].
    function mapToGoogleAIMessages(
      messages: Array<{ role: string; content: string }>,
    ): { systemInstruction: string | undefined; contents: Content[] } {
      const contents: Content[] = [];
      const systemParts: string[] = [];
      for (const msg of messages) {
        if (msg.role === 'system') systemParts.push(msg.content);
        else if (msg.role === 'user') contents.push({ role: 'user', parts: [{ text: msg.content }] });
        else if (msg.role === 'assistant') contents.push({ role: 'model', parts: [{ text: msg.content }] });
      }
      return {
        systemInstruction: systemParts.length ? systemParts.join(' ') : undefined,
        contents,
      };
    }

    // Convert a @google/genai response into LDAIMetrics. Passed to tracker.trackMetricsOf.
    function geminiMetrics(response: any): LDAIMetrics {
      const usage = response.usageMetadata;
      return {
        success: true,
        tokens: usage
          ? {
              input: usage.promptTokenCount || 0,
              output: usage.candidatesTokenCount || 0,
              total: usage.totalTokenCount || 0,
            }
          : undefined,
      };
    }

    async function main() {
      const genAI = new GoogleGenAI({ apiKey: GOOGLE_API_KEY });
      const ldClient: LDClient = init(SDK_KEY);
      await ldClient.waitForInitialization({ timeout: 10 });

      const aiClient: LDAIClient = initAi(ldClient);

      const context: LDContext = {
        kind: 'user',
        key: 'user-123',
        name: 'Sandy',
      };

      // ===================
      // COMPLETION MODE
      // ===================

      // Pass a default for improved resiliency when the AgentControl config is unavailable
      // or LaunchDarkly is unreachable; omit for a disabled default.
      // Example:
      //   const fallback = {
      //     enabled: true,
      //     model: { name: 'gemini-2.5-flash' },
      //     provider: { name: 'gemini' },
      //     messages: [{ role: 'system', content: 'You are a helpful assistant.' }],
      //   };
      //   const aiConfig = await aiClient.completionConfig(CONFIG_KEY, context, fallback, { topic: 'Python' });
      const aiConfig: LDAICompletionConfig = await aiClient.completionConfig(
        CONFIG_KEY,
        context,
        undefined,
        { topic: 'Python' },
      );

      if (aiConfig.enabled) {
        const tracker = aiConfig.createTracker();
        const { systemInstruction, contents } = mapToGoogleAIMessages(aiConfig.messages || []);
        contents.push({ role: 'user', parts: [{ text: 'How do I read a file in Python?' }] });

        await tracker.trackMetricsOf(geminiMetrics, () =>
          genAI.models.generateContent({
            model: aiConfig.model!.name,
            contents,
            config: {
              systemInstruction,
              ...geminiConfigFields((aiConfig.model?.parameters || {}) as Record<string, unknown>),
            },
          }),
        );
      }

      // ===================
      // AGENT MODE
      // ===================

      // Same fallback pattern as completion — omit the default for a disabled fallback.
      const agentConfig: LDAIAgentConfig = await aiClient.agentConfig(AGENT_CONFIG_KEY, context);

      if (agentConfig.enabled && agentConfig.instructions) {
        const tracker = agentConfig.createTracker();
        const ldParams = (agentConfig.model?.parameters || {}) as Record<string, unknown>;

        // LaunchDarkly returns attached tools under parameters.tools in OpenAI type=function shape.
        // Gemini wants { functionDeclarations: [{ name, description, parameters }] }.
        const ldTools = (ldParams.tools as any[] | undefined) ?? [];
        const tools = ldTools.length
          ? [{
              functionDeclarations: ldTools.map((t) => ({
                name: t.name as string,
                description: (t.description as string) ?? '',
                parameters: (t.parameters as any) ?? { type: 'object', properties: {} },
              })),
            }]
          : [];

        // Handlers stay in application code — LaunchDarkly governs the schema, the app owns execution.
        function getOrderStatus(orderId: string): string {
          const orders: Record<string, string> = {
            'ORD-123': 'Shipped — arrives Thursday',
            'ORD-456': 'Processing — estimated ship date: tomorrow',
            'ORD-789': 'Delivered on Monday',
          };
          return orders[orderId] || `No order found with ID ${orderId}`;
        }

        const toolHandlers: Record<string, (args: any) => string> = {
          get_order_status: (args) => getOrderStatus(args.order_id),
        };

        const chat = genAI.chats.create({
          model: agentConfig.model!.name,
          config: {
            systemInstruction: agentConfig.instructions,
            tools,
            ...geminiConfigFields(ldParams),
          },
        });

        // Agent loop: send, handle functionCalls, repeat
        const MAX_STEPS = 5;
        let messagePayload: any = "What's the status of order ORD-123?";
        for (let i = 0; i < MAX_STEPS; i++) {
          const response = await tracker.trackMetricsOf(geminiMetrics, () =>
            chat.sendMessage({ message: messagePayload }),
          );

          const calls = response.functionCalls || [];
          if (calls.length === 0) {
            break;
          }

          const functionResponseParts = calls.map((call) => {
            if (!toolHandlers[call.name!]) throw new Error(`Unknown tool: ${call.name}`);
            const result = toolHandlers[call.name!](call.args);
            return { functionResponse: { name: call.name!, response: { result } } };
          });
          messagePayload = functionResponseParts;
        }
      }

      // Always flush events before closing — trailing events are at risk of being
      // lost otherwise, in short-lived scripts and long-running services alike.
      await ldClient.flush();
      await ldClient.close();
    }

    main();
    ```
  </CodeGroup>
</Accordion>

## What to explore next

After you have the basic integration working, you can extend it with:

* [Tools](/docs/home/agentcontrol/tools) for calling external functions from your workflows
* [Online evaluations](/docs/home/agentcontrol/online-evaluations) to score response quality automatically
* [Experiments](/docs/home/agentcontrol/experimentation) to compare AgentControl config variations statistically
* [Agents](/docs/home/agentcontrol/agents) for multi-step workflows

For more AgentControl guides, read the other guides in the [AgentControl guides](/docs/guides/agentcontrol) section.

## Troubleshooting

If you are experiencing problems with your configuration, this section lists common errors and solutions.

### Metrics not appearing

If metrics do not appear on the **AI Insights** dashboard:

* Verify that you are calling `tracker.track_metrics_of()` (Python) or `tracker.trackMetricsOf()` (Node.js) with the correct metrics converter.
* Ensure you call `flush()` before closing the client, especially for short-lived scripts.
* Wait at least one minute for metrics to process.

### SDK initialization failures

If the LaunchDarkly SDK fails to initialize:

* Verify your SDK key is correct and matches the environment you are targeting.
* Check that your network can reach LaunchDarkly servers.
* Review the SDK logs for specific error messages.

### Config returns fallback value

If you always receive the fallback configuration:

* Verify targeting is enabled for your AgentControl config.
* Check that the AgentControl config key in your code matches the key in LaunchDarkly.
* Ensure your context matches the targeting rules.

### Google API errors

If you receive Google API errors:

* Verify your GEMINI\_API\_KEY or GOOGLE\_API\_KEY is set correctly.
* Check that the Gemini API is enabled for your API key.
* Ensure the model name in your AgentControl config matches an available Gemini model.

## Conclusion

In this guide, you connected a Google Gemini-powered application to LaunchDarkly AgentControl. You can now:

* Manage prompts and model settings in LaunchDarkly without code changes
* Track token usage, latency, and success rates automatically
* Use template variables to customize prompts per user

<View title="Developer">
  <Note>
    **Want to know more? Start a trial.**

    Your 14-day trial begins as soon as you sign up. Get started in minutes using the in-app Quickstart. You'll discover how easy it is to release, monitor, and optimize your software.<br /><br />

    Want to try it out? <a href="https://app.launchdarkly.com/signup">Start a trial</a>.
  </Note>
</View>

<View title="Federal docs" />

<View title="EU docs" />
