← Files LaunchDarklyARCHIVED FILE

skills/built-in-metrics/references/gemini-tracking.md

10.2 KB · Oct 2, 2026 · 00:24 UTC

↓ Download file

# Gemini Metrics Tracking

**There is no LaunchDarkly provider package for Gemini today** (neither Python nor Node). The canonical path is Tier 3: a small custom extractor composed with `trackMetricsOf`. The Gemini response shape is stable — `response.usage_metadata` / `response.usageMetadata` carries `prompt_token_count` / `promptTokenCount`, `candidates_token_count` / `candidatesTokenCount`, and `total_token_count` / `totalTokenCount` — so the extractor is three lines.

## Tier 1 is not available

`ManagedModel` does not currently ship a Gemini provider. If you need Tier 1 for a chat app, route via the LangChain provider package (`ChatGoogleGenerativeAI` under the hood), which restores the zero-tracker-call experience. See [langchain-tracking.md](langchain-tracking.md).

## Tier 3 — Custom extractor + `trackMetricsOf` (primary)

Gemini's API diverges from OpenAI's in three places that matter for a wrapper:

1. **System messages are a top-level field.** `GenerateContentConfig.system_instruction` / `systemInstruction` carries the system prompt; the `contents` array only holds `user` and `model` turns. You cannot put a `role: "system"` item in `contents`.
2. **Assistant messages use role `model`.** Convert `role: "assistant"` → `role: "model"` when mapping LD messages into Gemini's `contents`.
3. **Parameter names differ.** `max_tokens` on a LaunchDarkly variation (the snake_case key shown in the LD UI) becomes `max_output_tokens` on Python's `GenerateContentConfig`, or `maxOutputTokens` in Node. Other LD parameter names (`temperature`, `top_p`, `top_k`) either pass through or map with the same helper.

Two helpers absorb the divergence — a message splitter and a parameter remapper — and the metrics extractor sits on top.

**Python** — `google-genai`:

```python
from google import genai
from google.genai.types import Content, Part, GenerateContentConfig
from ldai.providers.types import LDAIMetrics, TokenUsage

gemini_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

def gemini_metrics(response) -> LDAIMetrics:
    usage = response.usage_metadata
    return LDAIMetrics(
        success=True,
        tokens=TokenUsage(
            total=usage.total_token_count or 0,
            input=usage.prompt_token_count or 0,
            output=usage.candidates_token_count or 0,
        ) if usage else None,
    )

def map_to_gemini_messages(ld_messages):
    """Split LD messages into (system_instruction, contents) for google-genai.
    System messages concatenate into the top-level system_instruction; user and
    assistant messages become Content items with role 'user' or 'model'."""
    system_parts: list[str] = []
    contents: list[Content] = []
    for m in ld_messages or []:
        if m.role == "system":
            system_parts.append(m.content)
        elif m.role == "user":
            contents.append(Content(role="user", parts=[Part(text=m.content)]))
        elif m.role == "assistant":
            contents.append(Content(role="model", parts=[Part(text=m.content)]))
    return (" ".join(system_parts) or None), contents

def gemini_config_kwargs(params):
    """Map config parameter names to google-genai's GenerateContentConfig.
    LaunchDarkly stores max_tokens (snake_case, matching the LD UI); Gemini's
    Python SDK expects max_output_tokens. Drop `tools` — they go on
    GenerateContentConfig.tools directly; leaving them here would double-pass."""
    mapping = {"max_tokens": "max_output_tokens"}
    return {mapping.get(k, k): v for k, v in (params or {}).items() if k != "tools"}

def call_with_tracking(ai_config, user_prompt: str) -> str | None:
    if not ai_config.enabled:
        return None

    system_instruction, contents = map_to_gemini_messages(ai_config.messages or [])
    contents.append(Content(role="user", parts=[Part(text=user_prompt)]))

    params = (ai_config.model.to_dict().get("parameters") if ai_config.model else None) or {}

    def call_gemini():
        return gemini_client.models.generate_content(
            model=ai_config.model.name,
            contents=contents,
            config=GenerateContentConfig(
                system_instruction=system_instruction,
                **gemini_config_kwargs(params),
            ),
        )

    tracker = ai_config.create_tracker()
    # Exceptions are tracked automatically — track_metrics_of catches
    # exceptions, records tracker.track_error(), and re-raises.
    response = tracker.track_metrics_of(gemini_metrics, call_gemini)
    return response.text
```

**Node** — `@google/genai`:

```typescript
import { GoogleGenAI, type Content } from '@google/genai';
import type { LDAIMetrics } from '@launchdarkly/server-sdk-ai';

const genAI = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });

const geminiMetrics = (response: any): LDAIMetrics => {
  const usage = response.usageMetadata;
  return {
    success: true,
    tokens: usage
      ? {
          total: usage.totalTokenCount ?? 0,
          input: usage.promptTokenCount ?? 0,
          output: usage.candidatesTokenCount ?? 0,
        }
      : undefined,
  };
};

function mapToGeminiMessages(
  ldMessages?: Array<{ role: string; content: string }>,
): { systemInstruction: string | undefined; contents: Content[] } {
  const contents: Content[] = [];
  const systemParts: string[] = [];
  for (const m of ldMessages ?? []) {
    if (m.role === 'system') systemParts.push(m.content);
    else if (m.role === 'user') contents.push({ role: 'user', parts: [{ text: m.content }] });
    else if (m.role === 'assistant') contents.push({ role: 'model', parts: [{ text: m.content }] });
  }
  return {
    systemInstruction: systemParts.length ? systemParts.join(' ') : undefined,
    contents,
  };
}

// Map config parameter names to @google/genai's GenerateContentConfig keys.
// LaunchDarkly stores max_tokens (snake_case, matching the LD UI); @google/genai
// expects maxOutputTokens. Drop `tools` — they go on GenerateContentConfig.tools
// directly; leaving them here would double-pass.
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]),
  );
}

async function callWithTracking(
  aiConfig: LDAICompletionConfig,
  userPrompt: string,
): Promise<string | null> {
  if (!aiConfig.enabled) return null;

  const { systemInstruction, contents } = mapToGeminiMessages(aiConfig.messages);
  contents.push({ role: 'user', parts: [{ text: userPrompt }] });

  const params = (aiConfig.model?.parameters ?? {}) as Record<string, unknown>;

  const tracker = aiConfig.createTracker();
  // Exceptions are tracked automatically — trackMetricsOf catches
  // exceptions, records tracker.trackError(), and re-throws.
  const response = await tracker.trackMetricsOf(
    geminiMetrics,
    () => genAI.models.generateContent({
      model: aiConfig.model!.name,
      contents,
      config: {
        systemInstruction,
        ...geminiConfigFields(params),
      },
    }),
  );
  return response.text ?? null;
}
```

Notes on the extractor shape:

- Gemini uses `snake_case` in Python (`prompt_token_count`) and `camelCase` in Node (`promptTokenCount`). The LD `TokenUsage` / `LDAIMetrics` type is the same in both.
- `total_token_count` already includes input + output from Google; do not recompute it.
- `success: true` in the extractor is not a lie — `trackMetricsOf` only calls the extractor on the success path. On the error path, `trackMetricsOf` records `trackError()` internally and re-throws; no caller-side catch block is required.

## Tools

LaunchDarkly stores attached tools on `ai_config.model.parameters.tools` in the flat `{type, name, description, parameters}` shape. Gemini's `GenerateContentConfig.tools` expects a list of `{function_declarations: [{name, description, parameters}]}` blocks (Python) or `{functionDeclarations: [...]}` (Node), so convert at runtime:

```python
ld_tools = (params.get("tools") or [])
gemini_tools = [
    {
        "function_declarations": [
            {
                "name": t["name"],
                "description": t.get("description", ""),
                "parameters": t.get("parameters", {"type": "object", "properties": {}}),
            }
            for t in ld_tools
        ],
    }
] if ld_tools else []
```

Tool handlers stay in your application code — LaunchDarkly stores the schema, your application owns the behavior. For the full agent loop pattern (`MAX_STEPS`, `functionCalls` handling, `tracker.track_tool_call`), see the agent-mode section of `tools`.

## Tier 2 option — route via LangChain

If the app can adopt LangChain, the LangChain provider package handles Gemini (via `@langchain/google-genai` / `langchain-google-genai`) through the standard `trackMetricsOf(getAIMetricsFromResponse, ...)` pattern. The provider package handles LaunchDarkly→LangChain provider-name mapping (for example, `"gemini"` → `"google_genai"`) and forwards all variation parameters automatically, so you do not need your own mapping helper. See [langchain-tracking.md](langchain-tracking.md).

## Tier 4 — Manual (streaming only)

Streaming Gemini needs manual TTFT tracking; the pattern is identical to OpenAI streaming. See [streaming-tracking.md](streaming-tracking.md).

## What NOT to do

- **Do not look for a `track_gemini_metrics` helper** — it does not exist. Gemini support lives in the extractor above.
- **Do not invent a provider package** like `@launchdarkly/server-sdk-ai-gemini` or `launchdarkly-server-sdk-ai-gemini`. Neither exists on npm or PyPI. Check [ai-providers in js-core](https://github.com/launchdarkly/js-core/tree/main/packages/ai-providers) and [python-server-sdk-ai/packages/ai-providers](https://github.com/launchdarkly/python-server-sdk-ai/tree/main/packages/ai-providers) before recommending one.
- **Do not put `role: "system"` items inside `contents`.** Gemini will either ignore them or error. The system prompt goes on `system_instruction` / `systemInstruction`.
- **Do not assume LaunchDarkly stores `maxTokens` (camelCase) as the parameter key.** The UI and the stored variation use `max_tokens`. The mapping helper renames it to `max_output_tokens` / `maxOutputTokens` for Gemini's SDK.

SHA-256: 1c00c3673b14ecedadff046c17126c4d79e88020c82aa3103ed697c6124c388e