mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
324 lines
7.9 KiB
TypeScript
324 lines
7.9 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2025 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* This file contains functions and types for converting Gemini API request/response
|
||
|
|
* formats to the OpenTelemetry semantic conventions for generative AI.
|
||
|
|
*
|
||
|
|
* @see https://github.com/open-telemetry/semantic-conventions/blob/8b4f210f43136e57c1f6f47292eb6d38e3bf30bb/docs/gen-ai/gen-ai-events.md
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { FinishReason } from '@google/genai';
|
||
|
|
import type {
|
||
|
|
Candidate,
|
||
|
|
Content,
|
||
|
|
ContentUnion,
|
||
|
|
Part,
|
||
|
|
PartUnion,
|
||
|
|
} from '@google/genai';
|
||
|
|
|
||
|
|
export function toInputMessages(contents: Content[]): InputMessages {
|
||
|
|
const messages: ChatMessage[] = [];
|
||
|
|
for (const content of contents) {
|
||
|
|
messages.push(toChatMessage(content));
|
||
|
|
}
|
||
|
|
return messages;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isPart(value: unknown): value is Part {
|
||
|
|
return (
|
||
|
|
typeof value === 'object' &&
|
||
|
|
value !== null &&
|
||
|
|
!Array.isArray(value) &&
|
||
|
|
!('parts' in value)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function toPart(part: PartUnion): Part {
|
||
|
|
if (typeof part === 'string') {
|
||
|
|
return { text: part };
|
||
|
|
}
|
||
|
|
return part;
|
||
|
|
}
|
||
|
|
|
||
|
|
function toContent(content: ContentUnion): Content | undefined {
|
||
|
|
if (typeof content === 'string') {
|
||
|
|
// 1. It's a string
|
||
|
|
return {
|
||
|
|
parts: [toPart(content)],
|
||
|
|
};
|
||
|
|
} else if (Array.isArray(content)) {
|
||
|
|
// 2. It's an array of parts (PartUnion[])
|
||
|
|
return {
|
||
|
|
parts: content.map(toPart),
|
||
|
|
};
|
||
|
|
} else if ('parts' in content) {
|
||
|
|
// 3. It's a Content object
|
||
|
|
return content;
|
||
|
|
} else if (isPart(content)) {
|
||
|
|
// 4. It's a single Part object (asserted with type guard)
|
||
|
|
return {
|
||
|
|
parts: [content],
|
||
|
|
};
|
||
|
|
} else {
|
||
|
|
// 5. Handle any other unexpected case
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toSystemInstruction(
|
||
|
|
systemInstruction?: ContentUnion,
|
||
|
|
): SystemInstruction | undefined {
|
||
|
|
const parts: AnyPart[] = [];
|
||
|
|
if (systemInstruction) {
|
||
|
|
const content = toContent(systemInstruction);
|
||
|
|
if (content && content.parts) {
|
||
|
|
for (const part of content.parts) {
|
||
|
|
parts.push(toOTelPart(part));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return parts;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toOutputMessages(candidates?: Candidate[]): OutputMessages {
|
||
|
|
const messages: OutputMessage[] = [];
|
||
|
|
if (candidates) {
|
||
|
|
for (const candidate of candidates) {
|
||
|
|
messages.push({
|
||
|
|
finish_reason: toOTelFinishReason(candidate.finishReason),
|
||
|
|
...toChatMessage(candidate.content),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return messages;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toFinishReasons(candidates?: Candidate[]): OTelFinishReason[] {
|
||
|
|
const reasons: OTelFinishReason[] = [];
|
||
|
|
if (candidates) {
|
||
|
|
for (const candidate of candidates) {
|
||
|
|
reasons.push(toOTelFinishReason(candidate.finishReason));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return reasons;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toOutputType(requested_mime?: string): string | undefined {
|
||
|
|
switch (requested_mime) {
|
||
|
|
// explictly support the known good values of responseMimeType
|
||
|
|
case 'text/plain':
|
||
|
|
return OTelOutputType.TEXT;
|
||
|
|
case 'application/json':
|
||
|
|
return OTelOutputType.JSON;
|
||
|
|
default:
|
||
|
|
// if none of the well-known values applies, a custom value may be used
|
||
|
|
return requested_mime;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toChatMessage(content?: Content): ChatMessage {
|
||
|
|
const message: ChatMessage = {
|
||
|
|
role: undefined,
|
||
|
|
parts: [],
|
||
|
|
};
|
||
|
|
if (content && content.parts) {
|
||
|
|
message.role = toOTelRole(content.role);
|
||
|
|
for (const part of content.parts) {
|
||
|
|
message.parts.push(toOTelPart(part));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return message;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toOTelPart(part: Part): AnyPart {
|
||
|
|
if (part.thought) {
|
||
|
|
if (part.text) {
|
||
|
|
return new ReasoningPart(part.text);
|
||
|
|
} else {
|
||
|
|
return new ReasoningPart('');
|
||
|
|
}
|
||
|
|
} else if (part.text) {
|
||
|
|
return new TextPart(part.text);
|
||
|
|
} else if (part.functionCall) {
|
||
|
|
return new ToolCallRequestPart(
|
||
|
|
part.functionCall.name,
|
||
|
|
part.functionCall.id,
|
||
|
|
JSON.stringify(part.functionCall.args),
|
||
|
|
);
|
||
|
|
} else if (part.functionResponse) {
|
||
|
|
return new ToolCallResponsePart(
|
||
|
|
JSON.stringify(part.functionResponse.response),
|
||
|
|
part.functionResponse.id,
|
||
|
|
);
|
||
|
|
} else if (part.executableCode) {
|
||
|
|
const { executableCode, ...unexpectedData } = part;
|
||
|
|
return new GenericPart('executableCode', {
|
||
|
|
code: executableCode.code,
|
||
|
|
language: executableCode.language,
|
||
|
|
...unexpectedData,
|
||
|
|
});
|
||
|
|
} else if (part.codeExecutionResult) {
|
||
|
|
const { codeExecutionResult, ...unexpectedData } = part;
|
||
|
|
return new GenericPart('codeExecutionResult', {
|
||
|
|
outcome: codeExecutionResult.outcome,
|
||
|
|
output: codeExecutionResult.output,
|
||
|
|
...unexpectedData,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// Assuming the above cases capture all the expected parts
|
||
|
|
// but adding a fallthrough just in case.
|
||
|
|
return new GenericPart('unknown', { ...part });
|
||
|
|
}
|
||
|
|
|
||
|
|
export enum OTelRole {
|
||
|
|
SYSTEM = 'system',
|
||
|
|
USER = 'user',
|
||
|
|
ASSISTANT = 'assistant',
|
||
|
|
TOOL = 'tool',
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toOTelRole(role?: string): OTelRole {
|
||
|
|
switch (role?.toLowerCase()) {
|
||
|
|
case 'system':
|
||
|
|
return OTelRole.SYSTEM;
|
||
|
|
// Our APIs seem to frequently use 'model'
|
||
|
|
case 'model':
|
||
|
|
return OTelRole.SYSTEM;
|
||
|
|
case 'user':
|
||
|
|
return OTelRole.USER;
|
||
|
|
case 'assistant':
|
||
|
|
return OTelRole.ASSISTANT;
|
||
|
|
case 'tool':
|
||
|
|
return OTelRole.TOOL;
|
||
|
|
default:
|
||
|
|
return OTelRole.SYSTEM;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export type InputMessages = ChatMessage[];
|
||
|
|
|
||
|
|
export enum OTelOutputType {
|
||
|
|
IMAGE = 'image',
|
||
|
|
JSON = 'json',
|
||
|
|
SPEECH = 'speech',
|
||
|
|
TEXT = 'text',
|
||
|
|
}
|
||
|
|
|
||
|
|
export enum OTelFinishReason {
|
||
|
|
STOP = 'stop',
|
||
|
|
LENGTH = 'length',
|
||
|
|
CONTENT_FILTER = 'content_filter',
|
||
|
|
TOOL_CALL = 'tool_call',
|
||
|
|
ERROR = 'error',
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toOTelFinishReason(finishReason?: string): OTelFinishReason {
|
||
|
|
switch (finishReason) {
|
||
|
|
// we have significantly more finish reasons than the spec
|
||
|
|
case FinishReason.FINISH_REASON_UNSPECIFIED:
|
||
|
|
return OTelFinishReason.STOP;
|
||
|
|
case FinishReason.STOP:
|
||
|
|
return OTelFinishReason.STOP;
|
||
|
|
case FinishReason.MAX_TOKENS:
|
||
|
|
return OTelFinishReason.LENGTH;
|
||
|
|
case FinishReason.SAFETY:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.RECITATION:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.LANGUAGE:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.OTHER:
|
||
|
|
return OTelFinishReason.STOP;
|
||
|
|
case FinishReason.BLOCKLIST:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.PROHIBITED_CONTENT:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.SPII:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.MALFORMED_FUNCTION_CALL:
|
||
|
|
return OTelFinishReason.ERROR;
|
||
|
|
case FinishReason.IMAGE_SAFETY:
|
||
|
|
return OTelFinishReason.CONTENT_FILTER;
|
||
|
|
case FinishReason.UNEXPECTED_TOOL_CALL:
|
||
|
|
return OTelFinishReason.ERROR;
|
||
|
|
default:
|
||
|
|
return OTelFinishReason.STOP;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface OutputMessage extends ChatMessage {
|
||
|
|
finish_reason: FinishReason | string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export type OutputMessages = OutputMessage[];
|
||
|
|
|
||
|
|
export type AnyPart =
|
||
|
|
| TextPart
|
||
|
|
| ToolCallRequestPart
|
||
|
|
| ToolCallResponsePart
|
||
|
|
| ReasoningPart
|
||
|
|
| GenericPart;
|
||
|
|
|
||
|
|
export type SystemInstruction = AnyPart[];
|
||
|
|
|
||
|
|
export interface ChatMessage {
|
||
|
|
role: string | undefined;
|
||
|
|
parts: AnyPart[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export class TextPart {
|
||
|
|
readonly type = 'text';
|
||
|
|
content: string;
|
||
|
|
|
||
|
|
constructor(content: string) {
|
||
|
|
this.content = content;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class ToolCallRequestPart {
|
||
|
|
readonly type = 'tool_call';
|
||
|
|
name?: string;
|
||
|
|
id?: string;
|
||
|
|
arguments?: string;
|
||
|
|
|
||
|
|
constructor(name?: string, id?: string, args?: string) {
|
||
|
|
this.name = name;
|
||
|
|
this.id = id;
|
||
|
|
this.arguments = args;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class ToolCallResponsePart {
|
||
|
|
readonly type = 'tool_call_response';
|
||
|
|
response?: string;
|
||
|
|
id?: string;
|
||
|
|
|
||
|
|
constructor(response?: string, id?: string) {
|
||
|
|
this.response = response;
|
||
|
|
this.id = id;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class ReasoningPart {
|
||
|
|
readonly type = 'reasoning';
|
||
|
|
content: string;
|
||
|
|
|
||
|
|
constructor(content: string) {
|
||
|
|
this.content = content;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class GenericPart {
|
||
|
|
type: string;
|
||
|
|
[key: string]: unknown;
|
||
|
|
|
||
|
|
constructor(type: string, data: { [key: string]: unknown }) {
|
||
|
|
this.type = type;
|
||
|
|
Object.assign(this, data);
|
||
|
|
}
|
||
|
|
}
|