refactor: update provider documentation and improve comments
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
/**
|
||||
* Gemini Provider implementation using Google's Generative AI API
|
||||
* Provides integration with Gemini models through a standardized interface
|
||||
* Google Gemini Provider Implementation
|
||||
*
|
||||
* This module provides integration with Google's Gemini models through the official
|
||||
* Generative AI SDK. Gemini offers cutting-edge AI capabilities including multimodal
|
||||
* understanding, advanced reasoning, and efficient text generation across various tasks.
|
||||
*
|
||||
* Key Features:
|
||||
* - Support for all Gemini model variants (Gemini 1.5 Pro, Flash, Pro Vision)
|
||||
* - Advanced streaming support with real-time token delivery
|
||||
* - Native multimodal capabilities (text, images, video)
|
||||
* - Sophisticated safety settings and content filtering
|
||||
* - Flexible generation configuration with fine-grained control
|
||||
*
|
||||
* Gemini-Specific Considerations:
|
||||
* - Uses "candidates" for response variants and "usageMetadata" for token counts
|
||||
* - Supports system instructions as separate parameter (not in conversation)
|
||||
* - Has sophisticated safety filtering with customizable thresholds
|
||||
* - Provides extensive generation configuration options
|
||||
* - Supports both single-turn and multi-turn conversations
|
||||
* - Offers advanced reasoning and coding capabilities
|
||||
*
|
||||
* @author Jan-Marlon Leibl
|
||||
* @version 1.0.0
|
||||
* @see https://ai.google.dev/docs
|
||||
*/
|
||||
|
||||
import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai';
|
||||
import type { Content, Part } from '@google/generative-ai';
|
||||
import type { Content, Part, GenerationConfig, SafetySetting } from '@google/generative-ai';
|
||||
import type {
|
||||
AIProviderConfig,
|
||||
CompletionParams,
|
||||
@@ -16,56 +38,213 @@ import type {
|
||||
import { BaseAIProvider } from './base.js';
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES AND INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration specific to Gemini provider
|
||||
* Configuration interface for Gemini provider with Google-specific options.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config: GeminiConfig = {
|
||||
* apiKey: process.env.GOOGLE_API_KEY!,
|
||||
* defaultModel: 'gemini-1.5-pro',
|
||||
* safetySettings: [
|
||||
* {
|
||||
* category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
* threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
||||
* }
|
||||
* ],
|
||||
* generationConfig: {
|
||||
* temperature: 0.7,
|
||||
* topP: 0.8,
|
||||
* topK: 40,
|
||||
* maxOutputTokens: 2048
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface GeminiConfig extends AIProviderConfig {
|
||||
/** Default model to use if not specified in requests (default: gemini-1.5-flash) */
|
||||
/**
|
||||
* Default Gemini model to use for requests.
|
||||
*
|
||||
* Recommended models:
|
||||
* - 'gemini-1.5-pro': Flagship model, best overall performance and multimodal
|
||||
* - 'gemini-1.5-flash': Faster and cheaper, good for simple tasks
|
||||
* - 'gemini-1.0-pro': Previous generation, cost-effective
|
||||
* - 'gemini-pro-vision': Specialized for vision tasks (legacy)
|
||||
*
|
||||
* @default 'gemini-1.5-flash'
|
||||
*/
|
||||
defaultModel?: string;
|
||||
/** Safety settings for content filtering */
|
||||
safetySettings?: any[];
|
||||
/** Generation configuration */
|
||||
|
||||
/**
|
||||
* Safety settings for content filtering and harm prevention.
|
||||
*
|
||||
* Gemini includes built-in safety filtering across multiple categories:
|
||||
* - Harassment and bullying
|
||||
* - Hate speech and discrimination
|
||||
* - Sexually explicit content
|
||||
* - Dangerous or harmful activities
|
||||
*
|
||||
* Each category can be configured with different blocking thresholds.
|
||||
*
|
||||
* @see https://ai.google.dev/docs/safety_setting
|
||||
*/
|
||||
safetySettings?: SafetySetting[];
|
||||
|
||||
/**
|
||||
* Generation configuration for controlling output characteristics.
|
||||
*
|
||||
* This allows fine-tuned control over the generation process including
|
||||
* creativity, diversity, length, and stopping conditions.
|
||||
*
|
||||
* @see https://ai.google.dev/docs/concepts#generation_configuration
|
||||
*/
|
||||
generationConfig?: {
|
||||
/** Controls randomness in generation (0.0 to 1.0) */
|
||||
temperature?: number;
|
||||
/** Controls nucleus sampling for diversity (0.0 to 1.0) */
|
||||
topP?: number;
|
||||
/** Controls top-k sampling for diversity (positive integer) */
|
||||
topK?: number;
|
||||
/** Maximum number of tokens to generate */
|
||||
maxOutputTokens?: number;
|
||||
/** Sequences that will stop generation when encountered */
|
||||
stopSequences?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini provider implementation
|
||||
* Processed messages structure for Gemini's conversation format.
|
||||
* Separates system instructions from conversational content.
|
||||
*/
|
||||
interface ProcessedMessages {
|
||||
/** System instruction for model behavior (if any) */
|
||||
systemInstruction?: string;
|
||||
/** Conversation content in Gemini's format */
|
||||
contents: Content[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GEMINI PROVIDER IMPLEMENTATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Google Gemini provider implementation.
|
||||
*
|
||||
* This class handles all interactions with Google's Gemini models through their
|
||||
* official Generative AI SDK. It provides optimized handling of Gemini's unique
|
||||
* features including multimodal inputs, safety filtering, and advanced generation control.
|
||||
*
|
||||
* Usage Pattern:
|
||||
* 1. Create instance with Google API key and configuration
|
||||
* 2. Call initialize() to set up client and validate credentials
|
||||
* 3. Use complete() or stream() for text generation
|
||||
* 4. Handle any AIProviderError exceptions appropriately
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const gemini = new GeminiProvider({
|
||||
* apiKey: process.env.GOOGLE_API_KEY!,
|
||||
* defaultModel: 'gemini-1.5-pro',
|
||||
* generationConfig: {
|
||||
* temperature: 0.7,
|
||||
* maxOutputTokens: 2048
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* await gemini.initialize();
|
||||
*
|
||||
* const response = await gemini.complete({
|
||||
* messages: [
|
||||
* { role: 'system', content: 'You are a helpful research assistant.' },
|
||||
* { role: 'user', content: 'Explain quantum computing.' }
|
||||
* ],
|
||||
* maxTokens: 1000,
|
||||
* temperature: 0.8
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class GeminiProvider extends BaseAIProvider {
|
||||
// ========================================================================
|
||||
// INSTANCE PROPERTIES
|
||||
// ========================================================================
|
||||
|
||||
/** Google Generative AI client instance (initialized during doInitialize) */
|
||||
private client: GoogleGenerativeAI | null = null;
|
||||
|
||||
/** Default model instance for requests */
|
||||
private model: GenerativeModel | null = null;
|
||||
|
||||
/** Default model identifier for requests */
|
||||
private readonly defaultModel: string;
|
||||
private readonly safetySettings?: any[];
|
||||
|
||||
/** Safety settings for content filtering */
|
||||
private readonly safetySettings?: SafetySetting[];
|
||||
|
||||
/** Generation configuration defaults */
|
||||
private readonly generationConfig?: any;
|
||||
|
||||
// ========================================================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Creates a new Gemini provider instance.
|
||||
*
|
||||
* @param config - Gemini-specific configuration options
|
||||
* @throws {AIProviderError} If configuration validation fails
|
||||
*/
|
||||
constructor(config: GeminiConfig) {
|
||||
super(config);
|
||||
|
||||
// Set Gemini-specific defaults
|
||||
this.defaultModel = config.defaultModel || 'gemini-1.5-flash';
|
||||
this.safetySettings = config.safetySettings;
|
||||
this.generationConfig = config.generationConfig;
|
||||
|
||||
// Validate model name format
|
||||
this.validateModelName(this.defaultModel);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PROTECTED TEMPLATE METHOD IMPLEMENTATIONS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the Gemini provider by setting up the Google Generative AI client
|
||||
* Initializes the Gemini provider by setting up the client and model.
|
||||
*
|
||||
* This method:
|
||||
* 1. Creates the Google Generative AI client with API key
|
||||
* 2. Sets up the default model with safety and generation settings
|
||||
* 3. Tests the connection with a minimal API call
|
||||
* 4. Validates API key permissions and model access
|
||||
*
|
||||
* @protected
|
||||
* @throws {Error} If client creation or connection validation fails
|
||||
*/
|
||||
protected async doInitialize(): Promise<void> {
|
||||
try {
|
||||
// Create Google Generative AI client
|
||||
this.client = new GoogleGenerativeAI(this.config.apiKey);
|
||||
|
||||
// Set up default model with configuration
|
||||
this.model = this.client.getGenerativeModel({
|
||||
model: this.defaultModel,
|
||||
safetySettings: this.safetySettings,
|
||||
generationConfig: this.generationConfig
|
||||
});
|
||||
|
||||
// Test the connection by making a simple request
|
||||
// Validate connection and permissions
|
||||
await this.validateConnection();
|
||||
|
||||
} catch (error) {
|
||||
// Clean up on failure
|
||||
this.client = null;
|
||||
this.model = null;
|
||||
|
||||
throw new AIProviderError(
|
||||
`Failed to initialize Gemini provider: ${(error as Error).message}`,
|
||||
AIErrorType.AUTHENTICATION,
|
||||
@@ -76,26 +255,30 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a completion using Gemini
|
||||
* Generates a text completion using Gemini's generation API.
|
||||
*
|
||||
* This method:
|
||||
* 1. Converts messages to Gemini's format with system instructions
|
||||
* 2. Chooses between single-turn and multi-turn conversation modes
|
||||
* 3. Makes API call with comprehensive error handling
|
||||
* 4. Formats response to standard interface
|
||||
*
|
||||
* @protected
|
||||
* @param params - Validated completion parameters
|
||||
* @returns Promise resolving to formatted completion response
|
||||
* @throws {Error} If API request fails
|
||||
*/
|
||||
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
|
||||
if (!this.client || !this.model) {
|
||||
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the model for this request (might be different from default)
|
||||
const model = params.model && params.model !== this.defaultModel
|
||||
? this.client.getGenerativeModel({
|
||||
model: params.model,
|
||||
safetySettings: this.safetySettings,
|
||||
generationConfig: this.buildGenerationConfig(params)
|
||||
})
|
||||
: this.model;
|
||||
|
||||
const model = this.getModelForRequest(params);
|
||||
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
||||
|
||||
// Create chat session or use generateContent
|
||||
// Choose appropriate generation method based on conversation length
|
||||
if (contents.length > 1) {
|
||||
// Multi-turn conversation - use chat session
|
||||
const chat = model.startChat({
|
||||
@@ -108,9 +291,10 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
if (!lastMessage) {
|
||||
throw new AIProviderError('No valid messages provided', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
const result = await chat.sendMessage(lastMessage.parts);
|
||||
|
||||
const result = await chat.sendMessage(lastMessage.parts);
|
||||
return this.formatCompletionResponse(result.response, params.model || this.defaultModel);
|
||||
|
||||
} else {
|
||||
// Single message - use generateContent
|
||||
const result = await model.generateContent({
|
||||
@@ -127,25 +311,30 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a streaming completion using Gemini
|
||||
* Generates a streaming text completion using Gemini's streaming API.
|
||||
*
|
||||
* This method:
|
||||
* 1. Sets up appropriate streaming mode based on conversation type
|
||||
* 2. Handles real-time stream chunks from Gemini
|
||||
* 3. Tracks token usage throughout the stream
|
||||
* 4. Yields formatted chunks with proper completion tracking
|
||||
*
|
||||
* @protected
|
||||
* @param params - Validated completion parameters
|
||||
* @returns AsyncIterable yielding completion chunks
|
||||
* @throws {Error} If streaming request fails
|
||||
*/
|
||||
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
||||
if (!this.client || !this.model) {
|
||||
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the model for this request
|
||||
const model = params.model && params.model !== this.defaultModel
|
||||
? this.client.getGenerativeModel({
|
||||
model: params.model,
|
||||
safetySettings: this.safetySettings,
|
||||
generationConfig: this.buildGenerationConfig(params)
|
||||
})
|
||||
: this.model;
|
||||
|
||||
const model = this.getModelForRequest(params);
|
||||
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
||||
|
||||
// Set up streaming based on conversation type
|
||||
let stream;
|
||||
if (contents.length > 1) {
|
||||
// Multi-turn conversation
|
||||
@@ -169,9 +358,221 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
});
|
||||
}
|
||||
|
||||
let fullText = '';
|
||||
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
// Process stream chunks
|
||||
yield* this.processStreamChunks(stream);
|
||||
|
||||
} catch (error) {
|
||||
throw this.handleGeminiError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PUBLIC INTERFACE METHODS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Returns comprehensive information about the Gemini provider.
|
||||
*
|
||||
* @returns Provider information including models, capabilities, and limits
|
||||
*/
|
||||
public getInfo(): ProviderInfo {
|
||||
return {
|
||||
name: 'Gemini',
|
||||
version: '1.0.0',
|
||||
models: [
|
||||
'gemini-1.5-pro', // Latest flagship model
|
||||
'gemini-1.5-flash', // Fast and efficient variant
|
||||
'gemini-1.0-pro', // Previous generation
|
||||
'gemini-pro-vision', // Vision-specialized (legacy)
|
||||
'gemini-1.5-pro-vision', // Latest vision model
|
||||
'gemini-1.0-pro-latest', // Latest 1.0 variant
|
||||
'gemini-1.5-pro-latest' // Latest 1.5 variant
|
||||
],
|
||||
maxContextLength: 1048576, // ~1M tokens for Gemini 1.5
|
||||
supportsStreaming: true,
|
||||
capabilities: {
|
||||
vision: true, // Advanced multimodal capabilities
|
||||
functionCalling: true, // Tool use and function calling
|
||||
jsonMode: true, // Structured JSON output
|
||||
systemMessages: true, // System instructions support
|
||||
reasoning: true, // Strong reasoning capabilities
|
||||
codeGeneration: true, // Excellent at programming tasks
|
||||
multimodal: true, // Text, image, video understanding
|
||||
safetyFiltering: true // Built-in content safety
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PRIVATE UTILITY METHODS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Validates the connection by making a minimal test request.
|
||||
*
|
||||
* @private
|
||||
* @throws {AIProviderError} If connection validation fails
|
||||
*/
|
||||
private async validateConnection(): Promise<void> {
|
||||
if (!this.model) {
|
||||
throw new Error('Model not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Make minimal request to test connection and permissions
|
||||
await this.model.generateContent({
|
||||
contents: [{ role: 'user', parts: [{ text: 'Hi' }] }],
|
||||
generationConfig: { maxOutputTokens: 1 }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
// Handle specific validation errors
|
||||
if (error.message?.includes('API key')) {
|
||||
throw new AIProviderError(
|
||||
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
if (error.message?.includes('quota') || error.message?.includes('billing')) {
|
||||
throw new AIProviderError(
|
||||
'API quota exceeded or billing issue. Please check your Google Cloud billing and API quotas.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
if (error.message?.includes('model')) {
|
||||
throw new AIProviderError(
|
||||
`Model '${this.defaultModel}' is not available. Please check the model name or your API access level.`,
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
error.status
|
||||
);
|
||||
}
|
||||
|
||||
// For other errors during validation, log but don't fail initialization
|
||||
console.warn('Gemini connection validation warning:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates model name format for Gemini models.
|
||||
*
|
||||
* @private
|
||||
* @param modelName - Model name to validate
|
||||
* @throws {AIProviderError} If model name format is invalid
|
||||
*/
|
||||
private validateModelName(modelName: string): void {
|
||||
if (!modelName || typeof modelName !== 'string') {
|
||||
throw new AIProviderError(
|
||||
'Model name must be a non-empty string',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
// Gemini model names follow specific patterns
|
||||
const validPatterns = [
|
||||
/^gemini-1\.5-pro(?:-latest|-vision)?$/, // e.g., gemini-1.5-pro, gemini-1.5-pro-latest
|
||||
/^gemini-1\.5-flash(?:-latest)?$/, // e.g., gemini-1.5-flash, gemini-1.5-flash-latest
|
||||
/^gemini-1\.0-pro(?:-latest|-vision)?$/, // e.g., gemini-1.0-pro, gemini-pro-vision
|
||||
/^gemini-pro(?:-vision)?$/, // Legacy names: gemini-pro, gemini-pro-vision
|
||||
/^models\/gemini-.+$/ // Full model path format
|
||||
];
|
||||
|
||||
const isValid = validPatterns.some(pattern => pattern.test(modelName));
|
||||
|
||||
if (!isValid) {
|
||||
console.warn(`Model name '${modelName}' doesn't match expected Gemini naming patterns. This may cause API errors.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate model instance for a request.
|
||||
*
|
||||
* @private
|
||||
* @param params - Completion parameters
|
||||
* @returns GenerativeModel instance
|
||||
*/
|
||||
private getModelForRequest(params: CompletionParams): GenerativeModel {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
|
||||
// Use default model if no specific model requested
|
||||
if (!params.model || params.model === this.defaultModel) {
|
||||
return this.model!;
|
||||
}
|
||||
|
||||
// Create new model instance for different model
|
||||
return this.client.getGenerativeModel({
|
||||
model: params.model,
|
||||
safetySettings: this.safetySettings,
|
||||
generationConfig: this.buildGenerationConfig(params)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts generic messages to Gemini's conversation format.
|
||||
*
|
||||
* Gemini handles system messages as separate system instructions
|
||||
* rather than part of the conversation flow.
|
||||
*
|
||||
* @private
|
||||
* @param messages - Input messages array
|
||||
* @returns Processed messages with system instruction separated
|
||||
*/
|
||||
private convertMessages(messages: AIMessage[]): ProcessedMessages {
|
||||
let systemInstruction: string | undefined;
|
||||
const contents: Content[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === 'system') {
|
||||
// Combine multiple system messages
|
||||
systemInstruction = systemInstruction
|
||||
? `${systemInstruction}\n\n${message.content}`
|
||||
: message.content;
|
||||
} else {
|
||||
contents.push({
|
||||
role: message.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{ text: message.content }]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { systemInstruction, contents };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds generation configuration from completion parameters.
|
||||
*
|
||||
* @private
|
||||
* @param params - Completion parameters
|
||||
* @returns Gemini generation configuration
|
||||
*/
|
||||
private buildGenerationConfig(params: CompletionParams): GenerationConfig {
|
||||
return {
|
||||
temperature: params.temperature ?? this.generationConfig?.temperature ?? 0.7,
|
||||
topP: params.topP ?? this.generationConfig?.topP,
|
||||
topK: this.generationConfig?.topK,
|
||||
maxOutputTokens: params.maxTokens ?? this.generationConfig?.maxOutputTokens ?? 1000,
|
||||
stopSequences: params.stopSequences ?? this.generationConfig?.stopSequences
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes streaming response chunks from Gemini API.
|
||||
*
|
||||
* @private
|
||||
* @param stream - Gemini streaming response
|
||||
* @returns AsyncIterable of formatted completion chunks
|
||||
*/
|
||||
private async *processStreamChunks(stream: any): AsyncIterable<CompletionChunk> {
|
||||
let fullText = '';
|
||||
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
try {
|
||||
// Process streaming chunks
|
||||
for await (const chunk of stream.stream) {
|
||||
const chunkText = chunk.text();
|
||||
fullText += chunkText;
|
||||
@@ -183,7 +584,7 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
};
|
||||
}
|
||||
|
||||
// Final chunk with usage info
|
||||
// Final chunk with usage information
|
||||
const finalResponse = await stream.response;
|
||||
const usageMetadata = finalResponse.usageMetadata;
|
||||
|
||||
@@ -198,128 +599,64 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.handleGeminiError(error as Error);
|
||||
throw new AIProviderError(
|
||||
`Streaming interrupted: ${(error as Error).message}`,
|
||||
AIErrorType.NETWORK,
|
||||
undefined,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the Gemini provider
|
||||
*/
|
||||
public getInfo(): ProviderInfo {
|
||||
return {
|
||||
name: 'Gemini',
|
||||
version: '1.0.0',
|
||||
models: [
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.0-pro',
|
||||
'gemini-1.0-pro-vision'
|
||||
],
|
||||
maxContextLength: 1000000, // Gemini 1.5 context length
|
||||
supportsStreaming: true,
|
||||
capabilities: {
|
||||
vision: true,
|
||||
functionCalling: true,
|
||||
systemMessages: true,
|
||||
multimodal: true,
|
||||
largeContext: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the connection by making a simple request
|
||||
*/
|
||||
private async validateConnection(): Promise<void> {
|
||||
if (!this.model) {
|
||||
throw new Error('Model not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Make a minimal request to validate credentials
|
||||
await this.model.generateContent('Hi');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('API key') || error.message?.includes('authentication')) {
|
||||
throw new AIProviderError(
|
||||
'Invalid API key. Please check your Google AI API key.',
|
||||
AIErrorType.AUTHENTICATION
|
||||
);
|
||||
}
|
||||
// For other errors during validation, we'll let initialization proceed
|
||||
// as they might be temporary issues
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our generic message format to Gemini's format
|
||||
* Gemini uses Contents with Parts and supports system instructions separately
|
||||
*/
|
||||
private convertMessages(messages: AIMessage[]): { systemInstruction?: string; contents: Content[] } {
|
||||
let systemInstruction: string | undefined;
|
||||
const contents: Content[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === 'system') {
|
||||
// Combine multiple system messages
|
||||
if (systemInstruction) {
|
||||
systemInstruction += '\n\n' + message.content;
|
||||
} else {
|
||||
systemInstruction = message.content;
|
||||
}
|
||||
} else {
|
||||
contents.push({
|
||||
role: message.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{ text: message.content }]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { systemInstruction, contents };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build generation config from completion parameters
|
||||
*/
|
||||
private buildGenerationConfig(params: CompletionParams) {
|
||||
return {
|
||||
temperature: params.temperature ?? 0.7,
|
||||
topP: params.topP,
|
||||
maxOutputTokens: params.maxTokens || 1000,
|
||||
stopSequences: params.stopSequences,
|
||||
...this.generationConfig
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Gemini's response to our standard format
|
||||
* Formats Gemini's response to our standard interface.
|
||||
*
|
||||
* @private
|
||||
* @param response - Raw Gemini API response
|
||||
* @param model - Model used for generation
|
||||
* @returns Formatted completion response
|
||||
* @throws {AIProviderError} If response format is unexpected
|
||||
*/
|
||||
private formatCompletionResponse(response: any, model: string): CompletionResponse {
|
||||
// Handle multiple text parts in the response
|
||||
const candidate = response.candidates?.[0];
|
||||
if (!candidate || !candidate.content?.parts?.[0]?.text) {
|
||||
if (!candidate) {
|
||||
throw new AIProviderError(
|
||||
'No content in Gemini response',
|
||||
'No candidates found in Gemini response',
|
||||
AIErrorType.UNKNOWN
|
||||
);
|
||||
}
|
||||
|
||||
const content = candidate.content.parts
|
||||
.filter((part: Part) => part.text)
|
||||
.map((part: Part) => part.text)
|
||||
const content = candidate.content;
|
||||
if (!content || !content.parts) {
|
||||
throw new AIProviderError(
|
||||
'No content found in Gemini response',
|
||||
AIErrorType.UNKNOWN
|
||||
);
|
||||
}
|
||||
|
||||
// Combine all text parts
|
||||
const text = content.parts
|
||||
.filter((part: any) => part.text)
|
||||
.map((part: any) => part.text)
|
||||
.join('');
|
||||
|
||||
const usageMetadata = response.usageMetadata;
|
||||
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
if (!text) {
|
||||
throw new AIProviderError(
|
||||
'No text content found in Gemini response',
|
||||
AIErrorType.UNKNOWN
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
model,
|
||||
content: text,
|
||||
model: model,
|
||||
usage: {
|
||||
promptTokens: usageMetadata?.promptTokenCount || 0,
|
||||
completionTokens: usageMetadata?.candidatesTokenCount || 0,
|
||||
totalTokens: usageMetadata?.totalTokenCount || 0
|
||||
promptTokens: response.usageMetadata?.promptTokenCount || 0,
|
||||
completionTokens: response.usageMetadata?.candidatesTokenCount || 0,
|
||||
totalTokens: response.usageMetadata?.totalTokenCount || 0
|
||||
},
|
||||
id: requestId,
|
||||
id: `gemini-${Date.now()}`,
|
||||
metadata: {
|
||||
finishReason: candidate.finishReason,
|
||||
safetyRatings: candidate.safetyRatings,
|
||||
@@ -329,7 +666,14 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Gemini-specific errors and convert them to our standard format
|
||||
* Handles and transforms Gemini-specific errors.
|
||||
*
|
||||
* This method maps Gemini's error responses to our standardized
|
||||
* error format, providing helpful context and actionable suggestions.
|
||||
*
|
||||
* @private
|
||||
* @param error - Original error from Gemini API
|
||||
* @returns Normalized AIProviderError
|
||||
*/
|
||||
private handleGeminiError(error: any): AIProviderError {
|
||||
if (error instanceof AIProviderError) {
|
||||
@@ -337,67 +681,124 @@ export class GeminiProvider extends BaseAIProvider {
|
||||
}
|
||||
|
||||
const message = error.message || 'Unknown Gemini API error';
|
||||
const status = error.status || error.statusCode;
|
||||
|
||||
// Handle common Gemini error patterns
|
||||
// Map Gemini-specific error patterns
|
||||
if (message.includes('API key')) {
|
||||
return new AIProviderError(
|
||||
'Authentication failed. Please check your Google AI API key.',
|
||||
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
undefined,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('quota') || message.includes('rate limit')) {
|
||||
if (message.includes('quota') || message.includes('limit exceeded')) {
|
||||
return new AIProviderError(
|
||||
'Rate limit exceeded. Please slow down your requests.',
|
||||
'API quota exceeded. Please check your Google Cloud quotas and billing.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
undefined,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('model') && message.includes('not found')) {
|
||||
if (message.includes('model not found') || message.includes('invalid model')) {
|
||||
return new AIProviderError(
|
||||
'Model not found. Please check the model name.',
|
||||
'Model not found. The specified Gemini model may not exist or be available to your API key.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
undefined,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('invalid') || message.includes('bad request')) {
|
||||
if (message.includes('safety') || message.includes('blocked')) {
|
||||
return new AIProviderError(
|
||||
`Invalid request: ${message}`,
|
||||
'Content blocked by safety filters. Please modify your input or adjust safety settings.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
undefined,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('network') || message.includes('connection')) {
|
||||
if (message.includes('length') || message.includes('token')) {
|
||||
return new AIProviderError(
|
||||
'Network error occurred. Please check your connection.',
|
||||
AIErrorType.NETWORK,
|
||||
undefined,
|
||||
'Input too long or exceeds token limits. Please reduce input size or max tokens.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (message.includes('timeout')) {
|
||||
return new AIProviderError(
|
||||
'Request timed out. Please try again.',
|
||||
'Request timed out. Gemini may be experiencing high load.',
|
||||
AIErrorType.TIMEOUT,
|
||||
undefined,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
return new AIProviderError(
|
||||
`Gemini API error: ${message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
undefined,
|
||||
error
|
||||
);
|
||||
if (message.includes('network') || message.includes('connection')) {
|
||||
return new AIProviderError(
|
||||
'Network error connecting to Gemini servers.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Handle HTTP status codes
|
||||
switch (status) {
|
||||
case 400:
|
||||
return new AIProviderError(
|
||||
`Invalid request to Gemini API: ${message}`,
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
return new AIProviderError(
|
||||
'Authentication failed with Gemini API. Please check your API key and permissions.',
|
||||
AIErrorType.AUTHENTICATION,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 404:
|
||||
return new AIProviderError(
|
||||
'Gemini API endpoint not found. Please check the model name and API version.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 429:
|
||||
return new AIProviderError(
|
||||
'Rate limit exceeded. Please reduce request frequency.',
|
||||
AIErrorType.RATE_LIMIT,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
return new AIProviderError(
|
||||
'Gemini service temporarily unavailable. Please try again in a few moments.',
|
||||
AIErrorType.NETWORK,
|
||||
status,
|
||||
error
|
||||
);
|
||||
|
||||
default:
|
||||
return new AIProviderError(
|
||||
`Gemini API error: ${message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user