@posthog/ai
Version:
PostHog Node.js AI integrations
1,044 lines (1,014 loc) • 34.2 kB
JavaScript
import { GoogleGenAI } from '@google/genai';
import { v4 } from 'uuid';
import { uuidv7 } from '@posthog/core';
// Type guards for safer type checking
const isString = value => {
return typeof value === 'string';
};
const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
class Base64Recognizer {
recognize(value, minLength) {
const dataUrl = DATA_URL_PREFIX_RE.exec(value);
if (dataUrl) return {
kind: 'data-url',
mediaType: dataUrl[1]
};
if (value.length < minLength) return {
kind: 'none'
};
const confidencePrefix = value.slice(0, minLength);
if (BASE64_ALPHABET_RE.test(confidencePrefix)) {
return {
kind: 'raw'
};
} else {
return {
kind: 'none'
};
}
}
}
const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'];
const STRONG_CONTEXT_KEYS = new Set(['data', 'file_data', 'fileData', 'image_url', 'imageUrl', 'video_url', 'videoUrl', 'audio', 'audio_data', 'audioData', 'inline_data', 'inlineData', 'source', 'result']);
const STRONG_CONTEXT_TYPES = new Set(['image', 'image_url', 'input_image', 'audio', 'input_audio', 'video', 'video_url', 'file', 'input_file', 'document', 'media', 'file-data']);
const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data']);
const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
class MediaTypeContext {
static EMPTY = new MediaTypeContext(undefined, undefined);
constructor(parent, key) {
this.parent = parent;
this.key = key;
}
inferMediaType() {
return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
}
inferFromSiblingMime() {
if (!this.parent) return undefined;
for (const hint of MIME_HINT_KEYS) {
const v = this.parent[hint];
if (typeof v === 'string') return v;
}
return undefined;
}
inferFromSiblingFormat() {
if (!this.parent) return undefined;
const fmt = this.parent.format;
if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {
return `audio/${fmt.toLowerCase()}`;
}
return undefined;
}
inferFromParentType() {
if (!this.parent) return undefined;
const t = this.parent.type;
if (typeof t !== 'string') return undefined;
if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image';
if (t === 'audio' || t === 'input_audio') return 'audio';
if (t === 'video' || t === 'video_url') return 'video';
if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream';
return undefined;
}
inferFromKey() {
if (!this.key) return undefined;
const key = this.key.toLowerCase();
if (key.includes('audio')) return 'audio';
if (key.includes('video')) return 'video';
if (key.includes('image')) return 'image';
if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
return undefined;
}
signalsBinary() {
if (this.parent) {
for (const hint of MIME_HINT_KEYS) {
if (typeof this.parent[hint] === 'string') return true;
}
const fmt = this.parent.format;
if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
const t = this.parent.type;
if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true;
}
if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
return false;
}
}
const STRONG_CONTEXT_MIN_LENGTH = 64;
const WEAK_CONTEXT_MIN_LENGTH = 1024;
class BinaryContentRedactor {
visited = new WeakSet();
constructor(recognizer = new Base64Recognizer()) {
this.recognizer = recognizer;
}
redact(value) {
if (this.isMultimodalEnabled()) return value;
this.visited = new WeakSet();
return this.walk(value, MediaTypeContext.EMPTY);
}
walk(value, ctx) {
if (value === null || value === undefined) return value;
if (typeof value === 'string') return this.redactString(value, ctx);
if (typeof value !== 'object') return value;
// Buffer extends Uint8Array, so this branch catches both.
if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
return this.placeholderFor(ctx.inferMediaType());
}
if (this.visited.has(value)) return null;
this.visited.add(value);
if (Array.isArray(value)) {
return value.map(item => this.walk(item, ctx));
}
const obj = value;
const out = {};
for (const k of Object.keys(obj)) {
out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
}
return out;
}
redactString(value, ctx) {
const minLength = ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
const recognition = this.recognizer.recognize(value, minLength);
switch (recognition.kind) {
case 'data-url':
return this.placeholderFor(recognition.mediaType);
case 'raw':
return this.placeholderFor(ctx.inferMediaType());
case 'none':
return value;
}
}
placeholderFor(mediaType) {
if (!mediaType) return '[base64 redacted]';
if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
return `[base64 ${mediaType} redacted]`;
}
isMultimodalEnabled() {
const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
}
}
const redactor = new BinaryContentRedactor();
function redactBase64DataUrl(str) {
return redactor.redact(str);
}
const sanitizeGemini = data => redactor.redact(data);
const TOKEN_PROPERTY_KEYS = new Set(['$ai_input_tokens', '$ai_output_tokens', '$ai_cache_read_input_tokens', '$ai_cache_creation_input_tokens', '$ai_total_tokens', '$ai_reasoning_tokens']);
function getTokensSource(posthogProperties) {
if (posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key))) {
return 'passthrough';
}
return 'sdk';
}
const STRING_FORMAT = 'utf8';
// Reused across calls to avoid per-invocation allocation; truncate() runs
// hundreds of times for prompts with many parts.
new TextEncoder();
new TextDecoder(STRING_FORMAT, {
fatal: false
});
/**
* Safely converts content to a string, preserving structure for objects/arrays.
* - If content is already a string, returns it as-is
* - If content is an object or array, stringifies it with JSON.stringify to preserve structure
* - Otherwise, converts to string with String()
*
* This prevents the "[object Object]" bug when objects are naively converted to strings.
*
* @param content - The content to convert to a string
* @returns A string representation that preserves structure for complex types
*/
function toContentString(content) {
if (typeof content === 'string') {
return content;
}
if (content !== undefined && content !== null && typeof content === 'object') {
try {
return JSON.stringify(content);
} catch {
// Fallback for circular refs, BigInt, or objects with throwing toJSON
return String(content);
}
}
return String(content);
}
const getModelParams = params => {
if (!params) {
return {};
}
const modelParams = {};
const paramKeys = ['temperature', 'max_tokens', 'max_completion_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'n', 'stop', 'stream', 'streaming', 'language', 'response_format', 'timestamp_granularities'];
for (const key of paramKeys) {
if (key in params && params[key] !== undefined) {
modelParams[key] = params[key];
}
}
return modelParams;
};
const buildInlineDataBlock = (mimeType, data) => {
if (mimeType.startsWith('audio/')) {
return {
type: 'audio',
mime_type: mimeType,
data
};
}
if (mimeType.startsWith('image/')) {
return {
type: 'image',
inline_data: {
mime_type: mimeType,
data
}
};
}
return {
type: 'document',
inline_data: {
mime_type: mimeType,
data
}
};
};
const formatResponseGemini = response => {
const output = [];
if (response.candidates && Array.isArray(response.candidates)) {
for (const candidate of response.candidates) {
if (candidate.content && candidate.content.parts) {
const content = [];
for (const part of candidate.content.parts) {
if (part.text) {
content.push({
type: 'text',
text: part.text
});
} else if (part.functionCall) {
content.push({
type: 'function',
function: {
name: part.functionCall.name,
arguments: part.functionCall.args
}
});
} else if (part.inlineData) {
// Handle inline data (images, audio, documents)
const mimeType = part.inlineData.mimeType || part.inlineData.mime_type || 'application/octet-stream';
let data = part.inlineData.data;
// Handle binary data (Uint8Array/Buffer -> base64)
if (data instanceof Uint8Array) {
if (typeof Buffer !== 'undefined') {
data = Buffer.from(data).toString('base64');
} else {
let binary = '';
for (let i = 0; i < data.length; i++) {
binary += String.fromCharCode(data[i]);
}
data = btoa(binary);
}
}
// Sanitize base64 data for images and other large inline data
data = redactBase64DataUrl(data);
content.push(buildInlineDataBlock(mimeType, data));
}
}
if (content.length > 0) {
output.push({
role: 'assistant',
content
});
}
} else if (candidate.text) {
output.push({
role: 'assistant',
content: [{
type: 'text',
text: candidate.text
}]
});
}
}
} else if (response.text) {
output.push({
role: 'assistant',
content: [{
type: 'text',
text: response.text
}]
});
}
return output;
};
const withPrivacyMode = (client, privacyMode, input) => {
return client.privacy_mode || privacyMode ? null : input;
};
/**
* Extract available tool calls from the request parameters.
* These are the tools provided to the LLM, not the tool calls in the response.
*/
const extractAvailableToolCalls = (provider, params) => {
{
if (params.config && params.config.tools) {
return params.config.tools;
}
return null;
}
};
let AIEvent = /*#__PURE__*/function (AIEvent) {
AIEvent["Generation"] = "$ai_generation";
AIEvent["Embedding"] = "$ai_embedding";
return AIEvent;
}({});
function sanitizeValues(obj) {
if (obj === undefined || obj === null) {
return obj;
}
const jsonSafe = JSON.parse(JSON.stringify(obj));
if (typeof jsonSafe === 'string') {
// Sanitize lone surrogates by round-tripping through UTF-8
return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
} else if (Array.isArray(jsonSafe)) {
return jsonSafe.map(sanitizeValues);
} else if (jsonSafe && typeof jsonSafe === 'object') {
return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
}
return jsonSafe;
}
const POSTHOG_PARAMS_MAP = {
posthogDistinctId: 'distinctId',
posthogTraceId: 'traceId',
posthogProperties: 'properties',
posthogPrivacyMode: 'privacyMode',
posthogGroups: 'groups',
posthogModelOverride: 'modelOverride',
posthogProviderOverride: 'providerOverride',
posthogCostOverride: 'costOverride',
posthogCaptureImmediate: 'captureImmediate'
};
function extractPosthogParams(body) {
const providerParams = {};
const posthogParams = {};
for (const [key, value] of Object.entries(body)) {
if (POSTHOG_PARAMS_MAP[key]) {
posthogParams[POSTHOG_PARAMS_MAP[key]] = value;
} else if (key.startsWith('posthog')) {
console.warn(`Unknown Posthog parameter ${key}`);
} else {
providerParams[key] = value;
}
}
return {
providerParams: providerParams,
posthogParams: addDefaults(posthogParams)
};
}
function addDefaults(params) {
return {
...params,
privacyMode: params.privacyMode ?? false,
traceId: params.traceId ?? v4()
};
}
var version = "7.19.5";
const DEFAULT_MAX_DEPTH = 3;
const MAX_STACK_LINES = 20;
function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
if (depth < 0 || value === null || typeof value !== 'object') {
return value;
}
if (value instanceof Error) {
const out = {
name: value.name,
message: value.message,
stack: truncateStack(value.stack)
};
for (const key of Object.keys(value)) {
out[key] = serializeError(value[key], depth - 1);
}
if (value.cause !== undefined) {
out.cause = serializeError(value.cause, depth - 1);
}
return out;
}
if (Array.isArray(value)) {
return value.map(item => serializeError(item, depth - 1));
}
return value;
}
function stringifyError(error) {
try {
return JSON.stringify(sanitizeValues(serializeError(error)));
} catch {
if (error instanceof Error) {
return JSON.stringify({
name: error.name,
message: error.message
});
}
return JSON.stringify({
message: String(error)
});
}
}
function truncateStack(stack) {
if (!stack) {
return stack;
}
const lines = stack.split('\n');
if (lines.length <= MAX_STACK_LINES) {
return stack;
}
return [...lines.slice(0, MAX_STACK_LINES), '... (truncated)'].join('\n');
}
/**
* Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape
* directly so that any caller — first-party SDK wrappers and external code
* alike — produces an identical event.
*/
/**
* Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
*
* This is the canonical primitive that every `@posthog/ai` wrapper
* (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
* external code can use it directly to instrument LLM calls made through
* arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
* same events the SDK wrappers produce.
*
* When `error` is set, the event is captured as an error. If the error is an
* object, it is mutated in place to set `__posthog_previously_captured_error`
* so callers can re-throw the original error reference safely.
*/
const captureAiGeneration = async (client, options) => {
if (!client.capture) {
return;
}
const traceId = options.traceId ?? v4();
const eventType = options.eventType ?? AIEvent.Generation;
const privacyMode = options.privacyMode ?? false;
const usage = options.usage ?? {};
const safeInput = sanitizeValues(options.input);
const safeOutput = sanitizeValues(options.output);
let httpStatus = options.httpStatus;
let errorData = {};
if (options.error) {
if (httpStatus === undefined) {
if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
httpStatus = options.error.status;
} else {
httpStatus = 500;
}
}
let exceptionId;
if (client.options?.enableExceptionAutocapture) {
exceptionId = uuidv7();
client.captureException(options.error, undefined, {
$ai_trace_id: traceId
}, exceptionId);
if (typeof options.error === 'object') {
options.error.__posthog_previously_captured_error = true;
}
}
errorData = {
$ai_is_error: true,
$ai_error: stringifyError(options.error),
$exception_event_id: exceptionId
};
}
httpStatus = httpStatus ?? 200;
let costOverrideData = {};
if (options.costOverride) {
const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
costOverrideData = {
$ai_input_cost_usd: inputCostUSD,
$ai_output_cost_usd: outputCostUSD,
$ai_total_cost_usd: inputCostUSD + outputCostUSD
};
}
const additionalTokenValues = {
...(usage.reasoningTokens ? {
$ai_reasoning_tokens: usage.reasoningTokens
} : {}),
...(usage.cacheReadInputTokens ? {
$ai_cache_read_input_tokens: usage.cacheReadInputTokens
} : {}),
...(usage.cacheCreationInputTokens ? {
$ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
} : {}),
...(usage.webSearchCount ? {
$ai_web_search_count: usage.webSearchCount
} : {}),
...(usage.rawUsage ? {
$ai_usage: usage.rawUsage
} : {})
};
const properties = {
$ai_lib: 'posthog-ai',
$ai_lib_version: version,
$ai_provider: options.providerOverride ?? options.provider,
$ai_model: options.modelOverride ?? options.model,
$ai_model_parameters: options.modelParameters ?? {},
$ai_input: withPrivacyMode(client, privacyMode, safeInput),
$ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
$ai_http_status: httpStatus,
$ai_input_tokens: usage.inputTokens ?? 0,
...(usage.outputTokens !== undefined ? {
$ai_output_tokens: usage.outputTokens
} : {}),
...additionalTokenValues,
$ai_latency: options.latency ?? 0,
...(options.timeToFirstToken !== undefined ? {
$ai_time_to_first_token: options.timeToFirstToken
} : {}),
$ai_trace_id: traceId,
$ai_base_url: options.baseURL ?? '',
...options.properties,
$ai_tokens_source: getTokensSource(options.properties),
...(options.distinctId ? {} : {
$process_person_profile: false
}),
...(options.stopReason ? {
$ai_stop_reason: options.stopReason
} : {}),
...(options.tools ? {
$ai_tools: options.tools
} : {}),
...errorData,
...costOverrideData
};
const event = {
distinctId: options.distinctId ?? traceId,
event: eventType,
properties,
groups: options.groups
};
if (options.captureImmediate) {
await client.captureImmediate(event);
} else {
client.capture(event);
}
};
class PostHogGoogleGenAI {
constructor(config) {
const {
posthog,
...geminiConfig
} = config;
this.phClient = posthog;
this.client = new GoogleGenAI(geminiConfig);
this.models = new WrappedModels(this.client, this.phClient);
}
}
class WrappedModels {
constructor(client, phClient) {
this.client = client;
this.phClient = phClient;
}
async generateContent(params) {
const {
providerParams: geminiParams,
posthogParams
} = extractPosthogParams(params);
const startTime = Date.now();
try {
const response = await this.client.models.generateContent(geminiParams);
const latency = (Date.now() - startTime) / 1000;
const availableTools = extractAvailableToolCalls('gemini', geminiParams);
const metadata = response.usageMetadata;
const finishReason = response.candidates?.[0]?.finishReason;
await captureAiGeneration(this.phClient, {
...posthogParams,
model: geminiParams.model,
provider: 'gemini',
input: this.formatInputForPostHog(geminiParams),
output: formatResponseGemini(response),
latency,
baseURL: 'https://generativelanguage.googleapis.com',
modelParameters: getModelParams(params),
httpStatus: 200,
usage: {
inputTokens: metadata?.promptTokenCount ?? 0,
outputTokens: metadata?.candidatesTokenCount ?? 0,
reasoningTokens: metadata?.thoughtsTokenCount ?? 0,
cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0,
webSearchCount: calculateGoogleWebSearchCount(response),
rawUsage: metadata
},
stopReason: finishReason ?? undefined,
tools: availableTools
});
return response;
} catch (error) {
const latency = (Date.now() - startTime) / 1000;
await captureAiGeneration(this.phClient, {
...posthogParams,
model: geminiParams.model,
provider: 'gemini',
input: this.formatInputForPostHog(geminiParams),
output: [],
latency,
baseURL: 'https://generativelanguage.googleapis.com',
modelParameters: getModelParams(params),
usage: {
inputTokens: 0,
outputTokens: 0
},
error
});
throw error;
}
}
async *generateContentStream(params) {
const {
providerParams: geminiParams,
posthogParams
} = extractPosthogParams(params);
const startTime = Date.now();
const accumulatedContent = [];
let firstTokenTime;
let stopReason;
let usage = {
inputTokens: 0,
outputTokens: 0,
webSearchCount: 0,
rawUsage: undefined
};
try {
const stream = await this.client.models.generateContentStream(geminiParams);
for await (const chunk of stream) {
// Track first token time when we get text content
if (firstTokenTime === undefined && chunk.text) {
firstTokenTime = Date.now();
}
const chunkWebSearchCount = calculateGoogleWebSearchCount(chunk);
if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
usage.webSearchCount = chunkWebSearchCount;
}
// Handle text content
if (chunk.text) {
// Find if we already have a text item to append to
let lastTextItem;
for (let i = accumulatedContent.length - 1; i >= 0; i--) {
if (accumulatedContent[i].type === 'text') {
lastTextItem = accumulatedContent[i];
break;
}
}
if (lastTextItem && lastTextItem.type === 'text') {
lastTextItem.text += chunk.text;
} else {
accumulatedContent.push({
type: 'text',
text: chunk.text
});
}
}
// Track finish reason from candidates
if (chunk.candidates?.[0]?.finishReason) {
stopReason = chunk.candidates[0].finishReason;
}
// Handle function calls from candidates
if (chunk.candidates && Array.isArray(chunk.candidates)) {
for (const candidate of chunk.candidates) {
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
// Type-safe check for functionCall
if ('functionCall' in part) {
if (firstTokenTime === undefined) {
firstTokenTime = Date.now();
}
const funcCall = part.functionCall;
if (funcCall?.name) {
accumulatedContent.push({
type: 'function',
function: {
name: funcCall.name,
arguments: funcCall.args || {}
}
});
}
}
}
}
}
}
// Update usage metadata - handle both old and new field names
if (chunk.usageMetadata) {
const metadata = chunk.usageMetadata;
usage = {
inputTokens: metadata.promptTokenCount ?? 0,
outputTokens: metadata.candidatesTokenCount ?? 0,
reasoningTokens: metadata.thoughtsTokenCount ?? 0,
cacheReadInputTokens: metadata.cachedContentTokenCount ?? 0,
webSearchCount: usage.webSearchCount,
rawUsage: metadata
};
}
yield chunk;
}
const latency = (Date.now() - startTime) / 1000;
const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
const availableTools = extractAvailableToolCalls('gemini', geminiParams);
// Format output similar to formatResponseGemini
const output = accumulatedContent.length > 0 ? [{
role: 'assistant',
content: accumulatedContent
}] : [];
await captureAiGeneration(this.phClient, {
...posthogParams,
model: geminiParams.model,
provider: 'gemini',
input: this.formatInputForPostHog(geminiParams),
output,
latency,
timeToFirstToken,
baseURL: 'https://generativelanguage.googleapis.com',
modelParameters: getModelParams(params),
httpStatus: 200,
usage: {
...usage,
webSearchCount: usage.webSearchCount,
rawUsage: usage.rawUsage
},
stopReason,
tools: availableTools
});
} catch (error) {
const latency = (Date.now() - startTime) / 1000;
await captureAiGeneration(this.phClient, {
...posthogParams,
model: geminiParams.model,
provider: 'gemini',
input: this.formatInputForPostHog(geminiParams),
output: [],
latency,
baseURL: 'https://generativelanguage.googleapis.com',
modelParameters: getModelParams(params),
usage: {
inputTokens: 0,
outputTokens: 0
},
error
});
throw error;
}
}
async embedContent(params) {
const {
providerParams: geminiParams,
posthogParams
} = extractPosthogParams(params);
const startTime = Date.now();
try {
const response = await this.client.models.embedContent(geminiParams);
const latency = (Date.now() - startTime) / 1000;
const inputTokens = extractEmbeddingTokenCount(response);
await captureAiGeneration(this.phClient, {
...posthogParams,
eventType: AIEvent.Embedding,
model: geminiParams.model,
provider: 'gemini',
input: withPrivacyMode(this.phClient, posthogParams.privacyMode ?? false, geminiParams.contents),
output: null,
latency,
baseURL: 'https://generativelanguage.googleapis.com',
modelParameters: getModelParams(params),
httpStatus: 200,
usage: {
inputTokens
}
});
return response;
} catch (error) {
const latency = (Date.now() - startTime) / 1000;
await captureAiGeneration(this.phClient, {
...posthogParams,
eventType: AIEvent.Embedding,
model: geminiParams.model,
provider: 'gemini',
input: withPrivacyMode(this.phClient, posthogParams.privacyMode ?? false, geminiParams.contents),
output: null,
latency,
baseURL: 'https://generativelanguage.googleapis.com',
modelParameters: getModelParams(params),
usage: {
inputTokens: 0
},
error
});
throw error;
}
}
formatPartsAsContentBlocks(parts) {
const blocks = [];
for (const part of parts) {
// Handle dict/object with text field
if (part && typeof part === 'object' && 'text' in part && part.text) {
blocks.push({
type: 'text',
text: String(part.text)
});
}
// Handle string parts
else if (typeof part === 'string') {
blocks.push({
type: 'text',
text: part
});
}
// Handle inlineData (images, audio, PDFs)
else if (part && typeof part === 'object' && 'inlineData' in part) {
const inlineData = part.inlineData;
const mimeType = inlineData.mimeType || inlineData.mime_type || 'application/octet-stream';
blocks.push(buildInlineDataBlock(mimeType, inlineData.data));
}
}
return blocks;
}
formatInput(contents) {
if (typeof contents === 'string') {
return [{
role: 'user',
content: contents
}];
}
if (Array.isArray(contents)) {
return contents.map(item => {
if (typeof item === 'string') {
return {
role: 'user',
content: item
};
}
if (item && typeof item === 'object') {
const obj = item;
if ('text' in obj && obj.text) {
return {
role: isString(obj.role) ? obj.role : 'user',
content: obj.text
};
}
if ('content' in obj && obj.content) {
// If content is a list, format it as content blocks
if (Array.isArray(obj.content)) {
const contentBlocks = this.formatPartsAsContentBlocks(obj.content);
return {
role: isString(obj.role) ? obj.role : 'user',
content: contentBlocks
};
}
return {
role: isString(obj.role) ? obj.role : 'user',
content: obj.content
};
}
if ('parts' in obj && Array.isArray(obj.parts)) {
const contentBlocks = this.formatPartsAsContentBlocks(obj.parts);
return {
role: isString(obj.role) ? obj.role : 'user',
content: contentBlocks
};
}
}
return {
role: 'user',
content: toContentString(item)
};
});
}
if (contents && typeof contents === 'object') {
const obj = contents;
if ('text' in obj && obj.text) {
return [{
role: 'user',
content: obj.text
}];
}
if ('content' in obj && obj.content) {
return [{
role: 'user',
content: obj.content
}];
}
}
return [{
role: 'user',
content: toContentString(contents)
}];
}
extractSystemInstruction(params) {
if (!params || typeof params !== 'object' || !params.config) {
return null;
}
const config = params.config;
if (!('systemInstruction' in config)) {
return null;
}
const systemInstruction = config.systemInstruction;
if (typeof systemInstruction === 'string') {
return systemInstruction;
}
if (systemInstruction && typeof systemInstruction === 'object' && 'text' in systemInstruction) {
return systemInstruction.text;
}
if (systemInstruction && typeof systemInstruction === 'object' && 'parts' in systemInstruction && Array.isArray(systemInstruction.parts)) {
for (const part of systemInstruction.parts) {
if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') {
return part.text;
}
}
}
if (Array.isArray(systemInstruction)) {
for (const part of systemInstruction) {
if (typeof part === 'string') {
return part;
}
if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') {
return part.text;
}
}
}
return null;
}
formatInputForPostHog(params) {
const sanitized = sanitizeGemini(params.contents);
const messages = this.formatInput(sanitized);
const systemInstruction = this.extractSystemInstruction(params);
if (systemInstruction) {
const hasSystemMessage = messages.some(msg => msg.role === 'system');
if (!hasSystemMessage) {
return [{
role: 'system',
content: systemInstruction
}, ...messages];
}
}
return messages;
}
}
/**
* Extract total token count from a Gemini embed_content response.
* Token counts are only available per-embedding via Vertex AI's statistics.tokenCount.
* Returns 0 if no token counts are available.
*/
function extractEmbeddingTokenCount(response) {
let total = 0;
if (response.embeddings) {
for (const embedding of response.embeddings) {
if (embedding.statistics?.tokenCount != null) {
total += embedding.statistics.tokenCount;
}
}
}
return total;
}
/**
* Detect if Google Search grounding was used in the response.
* Gemini bills per request that uses grounding, not per individual query.
* Returns 1 if grounding was used, 0 otherwise.
*/
function calculateGoogleWebSearchCount(response) {
if (!response || typeof response !== 'object' || !('candidates' in response)) {
return 0;
}
const candidates = response.candidates;
if (!Array.isArray(candidates)) {
return 0;
}
const hasGrounding = candidates.some(candidate => {
if (!candidate || typeof candidate !== 'object') {
return false;
}
// Check for grounding metadata
if ('groundingMetadata' in candidate && candidate.groundingMetadata) {
const metadata = candidate.groundingMetadata;
if (typeof metadata === 'object') {
// Check if web_search_queries exists and is non-empty
if ('webSearchQueries' in metadata && Array.isArray(metadata.webSearchQueries) && metadata.webSearchQueries.length > 0) {
return true;
}
// Check if grounding_chunks exists and is non-empty
if ('groundingChunks' in metadata && Array.isArray(metadata.groundingChunks) && metadata.groundingChunks.length > 0) {
return true;
}
}
}
// Check for google search in function calls
if ('content' in candidate && candidate.content && typeof candidate.content === 'object') {
const content = candidate.content;
if ('parts' in content && Array.isArray(content.parts)) {
return content.parts.some(part => {
if (!part || typeof part !== 'object' || !('functionCall' in part)) {
return false;
}
const functionCall = part.functionCall;
if (functionCall && typeof functionCall === 'object' && 'name' in functionCall && typeof functionCall.name === 'string') {
return functionCall.name.includes('google_search') || functionCall.name.includes('grounding');
}
return false;
});
}
}
return false;
});
return hasGrounding ? 1 : 0;
}
export { PostHogGoogleGenAI as Gemini, PostHogGoogleGenAI, WrappedModels, PostHogGoogleGenAI as default };
//# sourceMappingURL=index.mjs.map