jorel
Version:
A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.
387 lines (386 loc) • 16.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoogleGenerativeAIProvider = void 0;
const genai_1 = require("@google/genai");
const zod_1 = require("zod");
const __1 = require("..");
const shared_1 = require("../../shared");
const convert_llm_message_1 = require("./convert-llm-message");
class GoogleGenerativeAIProvider {
constructor(options = {}) {
this.name = options.name || GoogleGenerativeAIProvider.defaultName;
const apiKey = options.apiKey || process.env.GOOGLE_AI_API_KEY;
if (!apiKey) {
throw new Error("[GoogleGenerativeAIProvider] Missing API key. Either pass it as config.apiKey or set the GOOGLE_AI_API_KEY environment variable");
}
this.client = new genai_1.GoogleGenAI({ apiKey });
this.safetySettings = options.safetySettings;
}
async generateResponse(model, messages, config = {}) {
const start = Date.now();
try {
const { contents, systemInstruction } = (0, convert_llm_message_1.convertLlmMessagesToGoogleGenerativeAiMessages)(messages);
const requestConfig = this.prepareGenerationConfig(config);
// Add system instruction to config if present
if (systemInstruction) {
requestConfig.systemInstruction = systemInstruction;
}
// Add abort signal to config if present
if (config.abortSignal) {
requestConfig.abortSignal = config.abortSignal;
}
let result;
try {
result = await this.client.models.generateContent({
model,
contents,
config: requestConfig,
});
}
catch (error) {
if (error instanceof Error && error.message.toLowerCase().includes("aborted")) {
throw new shared_1.JorElAbortError("Request was aborted");
}
const { message, type } = this.parseGoogleApiError(error);
throw new shared_1.JorElLlmError(`[GoogleGenerativeAIProvider] Error generating content: ${message}`, type);
}
const candidate = result.candidates?.[0];
const contentParts = candidate?.content?.parts || [];
const textParts = contentParts.filter((p) => p.text && !p.thought);
const content = textParts.map((p) => p.text).join("");
const reasoningParts = contentParts.filter((p) => p.thought);
const reasoningContent = reasoningParts.length > 0 ? reasoningParts.map((p) => p.text).join("") : null;
const toolCalls = [];
for (const part of contentParts) {
if (part.functionCall) {
toolCalls.push({
id: (0, shared_1.generateUniqueId)(),
request: {
id: (0, shared_1.generateRandomId)(),
function: {
name: part.functionCall.name ?? "",
arguments: part.functionCall.args ?? {},
},
providerMetadata: part.thoughtSignature
? { google: { thoughtSignature: part.thoughtSignature } }
: undefined,
},
approvalState: config.tools?.getTool(part.functionCall.name ?? "")?.requiresConfirmation
? "requiresApproval"
: "noApprovalRequired",
executionState: "pending",
result: null,
error: null,
});
}
}
const durationMs = Date.now() - start;
return {
...(0, __1.generateAssistantMessage)(content, reasoningContent, toolCalls.length > 0 ? toolCalls : undefined),
meta: {
model,
provider: this.name,
temperature: config.temperature ?? undefined,
durationMs,
inputTokens: undefined,
outputTokens: undefined,
},
};
}
catch (error) {
if (error instanceof shared_1.JorElAbortError) {
throw error;
}
throw error;
}
}
async *generateResponseStream(model, messages, config = {}) {
const start = Date.now();
const { contents, systemInstruction } = (0, convert_llm_message_1.convertLlmMessagesToGoogleGenerativeAiMessages)(messages);
const requestConfig = this.prepareGenerationConfig(config);
// Add system instruction to config if present
if (systemInstruction) {
requestConfig.systemInstruction = systemInstruction;
}
// Add abort signal to config if present
if (config.abortSignal) {
requestConfig.abortSignal = config.abortSignal;
}
let streamResult;
try {
streamResult = await this.client.models.generateContentStream({
model,
contents,
config: requestConfig,
});
}
catch (error) {
const isAbort = error instanceof Error && (error.message.toLowerCase().includes("aborted") || error.name === "AbortError");
const stopReason = isAbort ? "userCancelled" : "generationError";
const { message: errorMessage, type: errorType } = stopReason === "generationError"
? this.parseGoogleApiError(error)
: { message: "", type: "unknown" };
yield {
type: "response",
role: "assistant",
content: "",
reasoningContent: null,
meta: {
model,
provider: this.name,
temperature: config.temperature ?? undefined,
durationMs: 0,
inputTokens: undefined,
outputTokens: undefined,
},
stopReason,
error: stopReason === "generationError"
? {
message: errorMessage,
type: errorType,
}
: undefined,
};
return;
}
let fullContent = "";
let fullReasoningContent = "";
const toolCalls = [];
let error;
try {
for await (const chunk of streamResult) {
const candidate = chunk.candidates?.[0];
const parts = candidate?.content?.parts || [];
const textParts = parts.filter((p) => p.text && !p.thought);
const chunkText = textParts.map((p) => p.text).join("");
fullContent += chunkText;
// Extract reasoning from parts
const reasoningParts = parts.filter((p) => p.thought);
const chunkReasoning = reasoningParts.map((p) => p.text).join("");
fullReasoningContent += chunkReasoning;
// Check for function calls
for (const part of parts) {
if (part.functionCall) {
// Check if this function call is already in our toolCalls array
const existingToolCall = toolCalls.find((tc) => tc.request.function.name === (part.functionCall.name ?? "") &&
JSON.stringify(tc.request.function.arguments) === JSON.stringify(part.functionCall.args ?? {}));
if (!existingToolCall) {
toolCalls.push({
id: (0, shared_1.generateUniqueId)(),
request: {
id: (0, shared_1.generateRandomId)(),
function: {
name: part.functionCall.name ?? "",
arguments: part.functionCall.args ?? {},
},
providerMetadata: part.thoughtSignature
? { google: { thoughtSignature: part.thoughtSignature } }
: undefined,
},
approvalState: config.tools?.getTool(part.functionCall.name ?? "")?.requiresConfirmation
? "requiresApproval"
: "noApprovalRequired",
executionState: "pending",
result: null,
error: null,
});
}
}
}
if (chunkText) {
yield { type: "chunk", content: chunkText, chunkId: (0, shared_1.generateUniqueId)() };
}
if (chunkReasoning) {
yield { type: "reasoningChunk", content: chunkReasoning, chunkId: (0, shared_1.generateUniqueId)() };
}
}
}
catch (e) {
// Map Google GenAI SDK errors to our error types
const { message: errorMessage, type } = this.parseGoogleApiError(e);
error = {
message: errorMessage,
type,
};
}
const durationMs = Date.now() - start;
// Determine stop reason and error message
const stopReason = config.abortSignal?.aborted ? "userCancelled" : error ? "generationError" : "completed";
// Log non-abort errors
if (error && stopReason === "generationError") {
config.logger?.error("GoogleGenerativeAIProvider", `Stream error: ${error.message}`);
}
const meta = {
model,
provider: this.name,
temperature: config.temperature ?? undefined,
durationMs,
inputTokens: undefined,
outputTokens: undefined,
};
// If we have tool calls, yield a response with tools
if (toolCalls.length > 0) {
yield {
type: "response",
role: "assistant_with_tools",
content: fullContent,
reasoningContent: fullReasoningContent || null,
toolCalls,
meta,
stopReason,
error: stopReason === "generationError" ? error : undefined,
};
}
else {
yield {
type: "response",
role: "assistant",
content: fullContent,
reasoningContent: fullReasoningContent || null,
meta,
stopReason,
error: stopReason === "generationError" ? error : undefined,
};
}
}
async getAvailableModels() {
return [];
}
async createEmbedding(model, text, abortSignal) {
let result;
try {
result = await this.client.models.embedContent({
model,
contents: [{ role: "user", parts: [{ text }] }],
config: abortSignal ? { abortSignal } : undefined,
});
}
catch (error) {
if (error.name === "AbortError" || (error.message && error.message.toLowerCase().includes("aborted"))) {
throw new shared_1.JorElAbortError("Request was aborted");
}
throw error;
}
if (!result.embeddings || result.embeddings.length === 0) {
throw new Error("No embedding returned");
}
return result.embeddings[0].values ?? [];
}
// Helper method for parsing Google API errors
parseGoogleApiError(error) {
let errorMessage;
let errorType = "unknown";
const status = error instanceof genai_1.ApiError ? error.status : undefined;
errorMessage = error instanceof Error ? error.message : String(error);
// Try to parse the error message if it's a JSON string from Google API
if (error instanceof genai_1.ApiError && errorMessage.startsWith("{")) {
try {
const parsedError = JSON.parse(errorMessage);
if (parsedError.error?.message) {
// The error message itself might be a JSON string
if (parsedError.error.message.startsWith("{")) {
try {
const innerError = JSON.parse(parsedError.error.message);
errorMessage = innerError.error?.message || parsedError.error.message;
}
catch {
errorMessage = parsedError.error.message;
}
}
else {
errorMessage = parsedError.error.message;
}
}
}
catch {
// If parsing fails, use the original message
}
}
// Map status codes to error types
if (status === 400) {
errorType = "invalid_request";
}
else if (status === 401) {
errorType = "authentication_error";
}
else if (status === 403) {
// 403 can mean quota exceeded or permission denied
const lowerMessage = errorMessage.toLowerCase();
if (lowerMessage.includes("quota") || lowerMessage.includes("resource exhausted")) {
errorType = "quota_exceeded";
}
else {
errorType = "authentication_error";
}
}
else if (status === 404) {
errorType = "invalid_request";
}
else if (status === 429) {
errorType = "rate_limit";
}
else if (status && status >= 500) {
errorType = "server_error";
}
return { message: errorMessage, type: errorType };
}
// Helper method for preparing request configuration
prepareGenerationConfig(config) {
const requestConfig = {
safetySettings: this.safetySettings,
};
if (config.reasoningEffort) {
requestConfig.thinkingConfig = {
includeThoughts: true,
thinkingBudget: config.reasoningEffort === "minimal" ? 0 : undefined,
thinkingLevel: config.reasoningEffort === "minimal"
? undefined
: config.reasoningEffort === "high" || config.reasoningEffort === "medium"
? genai_1.ThinkingLevel.HIGH
: genai_1.ThinkingLevel.LOW,
};
}
// Add generation config
if (config.temperature !== undefined || config.maxTokens !== undefined || config.json) {
requestConfig.temperature = config.temperature ?? undefined;
requestConfig.maxOutputTokens = config.maxTokens ?? undefined;
if (config.json) {
requestConfig.responseMimeType = "application/json";
if (typeof config.json !== "boolean") {
requestConfig.responseJsonSchema =
config.json instanceof zod_1.ZodObject ? (0, shared_1.zodSchemaToJsonSchema)(config.json) : config.json;
}
}
}
// Add tools
if (config.tools?.asLlmFunctions?.length) {
requestConfig.tools = [
{
functionDeclarations: config.tools.asLlmFunctions.map((f) => ({
name: f.function.name,
description: f.function.description,
parameters: f.function.parameters, // TODO: Improve types
})),
},
];
}
// Add tool config
if (config.tools?.hasTools && config.toolChoice) {
let mode = genai_1.FunctionCallingConfigMode.AUTO;
if (config.toolChoice === "none") {
mode = genai_1.FunctionCallingConfigMode.NONE;
}
else if (config.toolChoice === "required") {
mode = genai_1.FunctionCallingConfigMode.ANY;
}
requestConfig.toolConfig = {
functionCallingConfig: {
mode,
},
};
}
return requestConfig;
}
}
exports.GoogleGenerativeAIProvider = GoogleGenerativeAIProvider;
GoogleGenerativeAIProvider.defaultName = "google-generative-ai";