Skip to main content
Glama
firebase
by firebase
converters.ts18 kB
/** * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { GenkitError, ToolRequest, z } from 'genkit'; import { CandidateData, MessageData, ModelReference, Part, TextPart, ToolDefinition, } from 'genkit/model'; import { JSONPath } from 'jsonpath-plus'; import { FunctionCallingMode, FunctionDeclaration, GenerateContentCandidate as GeminiCandidate, Content as GeminiContent, Part as GeminiPart, PartialArg, Schema, SchemaType, VideoMetadata, } from './types.js'; export function toGeminiTool(tool: ToolDefinition): FunctionDeclaration { const declaration: FunctionDeclaration = { name: tool.name.replace(/\//g, '__'), // Gemini throws on '/' in tool name description: tool.description, parameters: toGeminiSchemaProperty(tool.inputSchema), }; return declaration; } function toGeminiSchemaProperty(property?: ToolDefinition['inputSchema']) { if (!property || !property.type) { return undefined; } const baseSchema: Schema = {}; if (property.description) { baseSchema.description = property.description; } if (property.enum) { baseSchema.enum = property.enum; } if (property.nullable) { baseSchema.nullable = property.nullable; } let propertyType; // nullable schema can ALSO be defined as, for example, type=['string','null'] if (Array.isArray(property.type)) { const types = property.type as string[]; if (types.includes('null')) { baseSchema.nullable = true; } // grab the type that's not `null` propertyType = types.find((t) => t !== 'null'); } else { propertyType = property.type; } if (propertyType === 'object') { const nestedProperties = {}; Object.keys(property.properties).forEach((key) => { nestedProperties[key] = toGeminiSchemaProperty(property.properties[key]); }); return { ...baseSchema, type: SchemaType.OBJECT, properties: nestedProperties, required: property.required, }; } else if (propertyType === 'array') { return { ...baseSchema, type: SchemaType.ARRAY, items: toGeminiSchemaProperty(property.items), }; } else { const schemaType = SchemaType[propertyType.toUpperCase()] as SchemaType; if (!schemaType) { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: `Unsupported property type ${propertyType.toUpperCase()}`, }); } return { ...baseSchema, type: schemaType, }; } } function toGeminiMedia(part: Part): GeminiPart { let media: GeminiPart; if (part.media?.url.startsWith('data:')) { // Inline data const dataUrl = part.media.url; const b64Data = dataUrl.substring(dataUrl.indexOf(',')! + 1); const contentType = part.media.contentType || dataUrl.substring(dataUrl.indexOf(':')! + 1, dataUrl.indexOf(';')); media = { inlineData: { mimeType: contentType, data: b64Data } }; } else { // File data if (!part.media?.contentType) { throw Error( 'Must supply a `contentType` when sending File URIs to Gemini.' ); } media = { fileData: { mimeType: part.media.contentType, fileUri: part.media.url, }, }; } // Video metadata if (part.metadata?.videoMetadata) { let videoMetadata = part.metadata.videoMetadata as VideoMetadata; media.videoMetadata = { ...videoMetadata }; } // Media resolution if (part.metadata?.mediaResolution) { media.mediaResolution = { ...part.metadata.mediaResolution }; } return maybeAddGeminiThoughtSignature(part, media); } function toGeminiToolRequest(part: Part): GeminiPart { if (!part.toolRequest?.input) { throw Error('Invalid ToolRequestPart: input was missing.'); } const functionCall: GeminiPart['functionCall'] = { name: part.toolRequest.name, args: part.toolRequest.input, }; if (part.toolRequest.ref) { functionCall.id = part.toolRequest.ref; } return maybeAddGeminiThoughtSignature(part, { functionCall }); } function toGeminiToolResponse(part: Part): GeminiPart { if (!part.toolResponse?.output) { throw Error('Invalid ToolResponsePart: output was missing.'); } const functionResponse: GeminiPart['functionResponse'] = { name: part.toolResponse.name, response: { name: part.toolResponse.name, content: part.toolResponse.output, }, }; if (part.toolResponse.content) { functionResponse.parts = part.toolResponse.content.map(toGeminiPart); } if (part.toolResponse.ref) { functionResponse.id = part.toolResponse.ref; } return maybeAddGeminiThoughtSignature(part, { functionResponse, }); } function toGeminiReasoning(part: Part): GeminiPart { const out: GeminiPart = { thought: true }; if (part.reasoning?.length) { out.text = part.reasoning; } return maybeAddGeminiThoughtSignature(part, out); } function toGeminiCustom(part: Part): GeminiPart { if (part.custom?.codeExecutionResult) { return maybeAddGeminiThoughtSignature(part, { codeExecutionResult: part.custom.codeExecutionResult, }); } if (part.custom?.executableCode) { return maybeAddGeminiThoughtSignature(part, { executableCode: part.custom.executableCode, }); } throw new Error('Unsupported Custom Part type'); } function toGeminiText(part: Part): GeminiPart { return maybeAddGeminiThoughtSignature(part, { text: part.text ?? '' }); } function maybeAddGeminiThoughtSignature( part: Part, geminiPart: GeminiPart ): GeminiPart { if (part.metadata?.thoughtSignature) { return { ...geminiPart, thoughtSignature: part.metadata.thoughtSignature as string, }; } return geminiPart; } function toGeminiPart(part: Part): GeminiPart { if (typeof part.text === 'string') { return toGeminiText(part); } if (part.media) { return toGeminiMedia(part); } if (part.toolRequest) { return toGeminiToolRequest(part); } if (part.toolResponse) { return toGeminiToolResponse(part); } if (typeof part.reasoning === 'string') { return toGeminiReasoning(part); } if (part.custom) { return toGeminiCustom(part); } throw new Error('Unsupported Part type ' + JSON.stringify(part)); } function toGeminiRole( role: MessageData['role'], model?: ModelReference<z.ZodTypeAny> ): string { switch (role) { case 'user': return 'user'; case 'model': return 'model'; case 'system': if (model?.info?.supports?.systemRole) { // We should have already pulled out the supported system messages, // anything remaining is unsupported; throw an error. throw new Error( 'system role is only supported for a single message in the first position' ); } else { throw new Error('system role is not supported'); } case 'tool': return 'function'; default: return 'user'; } } export function toGeminiMessage( message: MessageData, model?: ModelReference<z.ZodTypeAny> ): GeminiContent { let sortedParts = message.content; if (message.role === 'tool') { sortedParts = [...message.content].sort((a, b) => { const aRef = a.toolResponse?.ref; const bRef = b.toolResponse?.ref; if (!aRef && !bRef) return 0; if (!aRef) return 1; if (!bRef) return -1; return parseInt(aRef, 10) - parseInt(bRef, 10); }); } return { role: toGeminiRole(message.role, model), parts: sortedParts.map(toGeminiPart), }; } export function toGeminiSystemInstruction(message: MessageData): GeminiContent { return { role: 'user', parts: message.content.map(toGeminiPart), }; } /** * Converts mode from either genkit tool choice (lowercase) * or functionCallingConfig (uppercase). * @param from The mode to convert from * @returns */ export function toGeminiFunctionModeEnum( from?: string //genkitMode: 'auto' | 'required' | 'none' ): FunctionCallingMode | undefined { if (from === undefined) { return undefined; } switch (from) { case 'MODE_UNSPECIFIED': { return FunctionCallingMode.MODE_UNSPECIFIED; } case 'required': case 'ANY': { return FunctionCallingMode.ANY; } case 'auto': case 'AUTO': { return FunctionCallingMode.AUTO; } case 'none': case 'NONE': { return FunctionCallingMode.NONE; } default: throw new Error(`unsupported function calling mode: ${from}`); } } function fromGeminiFinishReason( reason: GeminiCandidate['finishReason'] ): CandidateData['finishReason'] { if (!reason) return 'unknown'; switch (reason) { case 'STOP': return 'stop'; case 'MAX_TOKENS': return 'length'; case 'SAFETY': // blocked for safety case 'RECITATION': // blocked for reciting training data case 'LANGUAGE': // blocked for using an unsupported language case 'BLOCKLIST': // blocked for forbidden terms case 'PROHIBITED_CONTENT': // blocked for potentially containing prohibited content case 'SPII': // blocked for potentially containing Sensitive Personally Identifiable Information return 'blocked'; case 'MALFORMED_FUNCTION_CALL': case 'MISSING_THOUGHT_SIGNATURE': case 'OTHER': return 'other'; default: return 'unknown'; } } function maybeAddThoughtSignature(geminiPart: GeminiPart, part: Part): Part { if (geminiPart.thoughtSignature) { return { ...part, metadata: { ...part?.metadata, thoughtSignature: geminiPart.thoughtSignature, }, }; } return part; } function fromGeminiThought(part: GeminiPart): Part { return maybeAddThoughtSignature(part, { reasoning: part.text || '', }); } function fromGeminiInlineData(part: GeminiPart): Part { // Check if the required properties exist if ( !part.inlineData || !part.inlineData.hasOwnProperty('mimeType') || !part.inlineData.hasOwnProperty('data') ) { throw new Error('Invalid GeminiPart: missing required properties'); } const { mimeType, data } = part.inlineData; // Combine data and mimeType into a data URL const dataUrl = `data:${mimeType};base64,${data}`; return maybeAddThoughtSignature(part, { media: { url: dataUrl, contentType: mimeType, }, }); } function fromGeminiFileData(part: GeminiPart): Part { if ( !part.fileData || !part.fileData.hasOwnProperty('mimeType') || !part.fileData.hasOwnProperty('fileUri') ) { throw new Error( 'Invalid Gemini File Data Part: missing required properties' ); } return maybeAddThoughtSignature(part, { media: { url: part.fileData?.fileUri, contentType: part.fileData?.mimeType, }, }); } /** * Applies Gemini partial args to the target object. * * https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/Content#PartialArg */ export function applyGeminiPartialArgs( target: object, partialArgs: PartialArg[] ) { for (const partialArg of partialArgs) { if (!partialArg.jsonPath) { continue; } let value: boolean | string | number | null | undefined; if (partialArg.boolValue !== undefined) { value = partialArg.boolValue; } else if (partialArg.nullValue !== undefined) { value = null; } else if (partialArg.numberValue !== undefined) { value = partialArg.numberValue; } else if (partialArg.stringValue !== undefined) { value = partialArg.stringValue; } if (value === undefined) { continue; } let current: any = target; const path = JSONPath.toPathArray(partialArg.jsonPath); // ex: for path '$.data[0][0]' toPathArray returns: ['$', 'data', '0', '0'] // we skip the first (root) reference and dereference the rest. for (let i = 1; i < path.length - 1; i++) { const key = path[i]; const nextKey = path[i + 1]; if (current[key] === undefined) { if (!isNaN(parseInt(nextKey, 10))) { current[key] = []; } else { current[key] = {}; } } current = current[key]; } const finalKey = path[path.length - 1]; if ( partialArg.stringValue !== undefined && typeof current[finalKey] === 'string' ) { current[finalKey] += partialArg.stringValue; } else { current[finalKey] = value as any; } } } function fromGeminiFunctionCall( part: GeminiPart, previousChunks?: CandidateData[] ): Part { if (!part.functionCall) { throw Error( 'Invalid Gemini Function Call Part: missing function call data' ); } const req: Partial<ToolRequest> = { name: part.functionCall.name, input: part.functionCall.args, }; if (part.functionCall.id) { req.ref = part.functionCall.id; } if (part.functionCall.willContinue) { req.partial = true; } handleFunctionCallPartials(req, part, previousChunks); const toolRequest: Part = { toolRequest: req as ToolRequest }; return maybeAddThoughtSignature(part, toolRequest); } function handleFunctionCallPartials( req: Partial<ToolRequest>, part: GeminiPart, previousChunks?: CandidateData[] ) { if (!part.functionCall) { throw Error( 'Invalid Gemini Function Call Part: missing function call data' ); } // we try to find if there's a previous partial tool request part. const prevPart = previousChunks?.at(-1)?.message.content?.at(-1); const prevPartialToolRequestPart = prevPart?.toolRequest && prevPart?.toolRequest.partial ? prevPart : undefined; // if the current functionCall has partialArgs, we try to apply the diff to the // potentially including the previous partial part. if (part.functionCall.partialArgs) { const newInput = prevPartialToolRequestPart?.toolRequest?.input ? JSON.parse(JSON.stringify(prevPartialToolRequestPart.toolRequest.input)) : {}; applyGeminiPartialArgs(newInput, part.functionCall.partialArgs); req.input = newInput; } // If there's a previous partial part, we copy some fields over, because the // API will not return these. if (prevPartialToolRequestPart) { if (!req.name) { req.name = prevPartialToolRequestPart.toolRequest.name; } if (!req.ref) { req.ref = prevPartialToolRequestPart.toolRequest.ref; } // This is a special case for the final partial function call chunk from the API, // it will have nothing... so we need to make sure to copy the input // from the previous. if (req.input === undefined) { req.input = prevPartialToolRequestPart.toolRequest.input; } } } function fromGeminiFunctionResponse(part: GeminiPart): Part { if (!part.functionResponse) { throw new Error( 'Invalid Gemini Function Call Part: missing function call data' ); } const toolResponse: Part = { toolResponse: { name: part.functionResponse.name.replace(/__/g, '/'), // restore slashes output: part.functionResponse.response, }, }; if (part.functionResponse.id) { toolResponse.toolResponse.ref = part.functionResponse.id; } return maybeAddThoughtSignature(part, toolResponse); } function fromExecutableCode(part: GeminiPart): Part { if (!part.executableCode) { throw new Error('Invalid GeminiPart: missing executableCode'); } return maybeAddThoughtSignature(part, { custom: { executableCode: { language: part.executableCode.language, code: part.executableCode.code, }, }, }); } function fromCodeExecutionResult(part: GeminiPart): Part { if (!part.codeExecutionResult) { throw new Error('Invalid GeminiPart: missing codeExecutionResult'); } return maybeAddThoughtSignature(part, { custom: { codeExecutionResult: { outcome: part.codeExecutionResult.outcome, output: part.codeExecutionResult.output, }, }, }); } function fromGeminiText(part: GeminiPart): Part { return maybeAddThoughtSignature(part, { text: part.text } as TextPart); } function fromGeminiPart( part: GeminiPart, previousChunks?: CandidateData[] ): Part { if (part.thought) return fromGeminiThought(part as any); if (typeof part.text === 'string') return fromGeminiText(part); if (part.inlineData) return fromGeminiInlineData(part); if (part.fileData) return fromGeminiFileData(part); if (part.functionCall) return fromGeminiFunctionCall(part, previousChunks); if (part.functionResponse) return fromGeminiFunctionResponse(part); if (part.executableCode) return fromExecutableCode(part); if (part.codeExecutionResult) return fromCodeExecutionResult(part); throw new Error('Unsupported GeminiPart type ' + JSON.stringify(part)); } export function fromGeminiCandidate( candidate: GeminiCandidate, previousChunks?: CandidateData[] ): CandidateData { const parts = candidate.content?.parts || []; const genkitCandidate: CandidateData = { index: candidate.index || 0, message: { role: 'model', content: parts // the model sometimes returns empty parts, ignore those. .filter((p) => Object.keys(p).length > 0) .map((part) => fromGeminiPart(part, previousChunks)), }, finishReason: fromGeminiFinishReason(candidate.finishReason), finishMessage: candidate.finishMessage, custom: { safetyRatings: candidate.safetyRatings, citationMetadata: candidate.citationMetadata, }, }; return genkitCandidate; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/firebase/genkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server