Add Google Gemini as AI provider

- Add gemini.py provider using google-genai SDK
- Update config.py with gemini provider and GEMINI_API_KEY
- Update ai_service.py factory to support gemini
- Add google-genai to requirements.txt
- Update .env.example, README.md, and CLAUDE.md documentation
This commit is contained in:
2026-01-11 20:23:03 +01:00
parent b05fa034a6
commit 5761529c7f
8 changed files with 89 additions and 7 deletions

View File

@@ -7,7 +7,7 @@ DISCORD_TOKEN=your_discord_bot_token_here
# =========================================== # ===========================================
# AI Provider Configuration # AI Provider Configuration
# =========================================== # ===========================================
# Available providers: "openai", "openrouter", "anthropic" # Available providers: "openai", "openrouter", "anthropic", "gemini"
AI_PROVIDER=openai AI_PROVIDER=openai
# Model to use (e.g., gpt-4o, gpt-4o-mini, claude-3-5-sonnet, etc.) # Model to use (e.g., gpt-4o, gpt-4o-mini, claude-3-5-sonnet, etc.)
@@ -17,6 +17,7 @@ AI_MODEL=gpt-4o
OPENAI_API_KEY=sk-xxx OPENAI_API_KEY=sk-xxx
OPENROUTER_API_KEY=sk-or-xxx OPENROUTER_API_KEY=sk-or-xxx
ANTHROPIC_API_KEY=sk-ant-xxx ANTHROPIC_API_KEY=sk-ant-xxx
GEMINI_API_KEY=xxx
# Maximum tokens in AI response (100-4096) # Maximum tokens in AI response (100-4096)
AI_MAX_TOKENS=1024 AI_MAX_TOKENS=1024

View File

@@ -25,9 +25,10 @@ This is a Discord bot that responds to @mentions with AI-generated responses (mu
### Provider Pattern ### Provider Pattern
The AI system uses a provider abstraction pattern: The AI system uses a provider abstraction pattern:
- `services/providers/base.py` defines `AIProvider` abstract class with `generate()` method - `services/providers/base.py` defines `AIProvider` abstract class with `generate()` method
- `services/providers/openai.py`, `openrouter.py`, `anthropic.py` implement the interface - `services/providers/openai.py`, `openrouter.py`, `anthropic.py`, `gemini.py` implement the interface
- `services/ai_service.py` is the factory that creates the correct provider based on `AI_PROVIDER` env var - `services/ai_service.py` is the factory that creates the correct provider based on `AI_PROVIDER` env var
- OpenRouter uses OpenAI's client with a different base URL - OpenRouter uses OpenAI's client with a different base URL
- Gemini uses the `google-genai` SDK
### Cog System ### Cog System
Discord functionality is in `cogs/`: Discord functionality is in `cogs/`:
@@ -45,4 +46,4 @@ All config flows through `config.py` using pydantic-settings. The `settings` sin
## Environment Variables ## Environment Variables
Required: `DISCORD_TOKEN`, plus one of `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, or `ANTHROPIC_API_KEY` depending on `AI_PROVIDER` setting. Required: `DISCORD_TOKEN`, plus one of `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY` depending on `AI_PROVIDER` setting.

View File

@@ -4,7 +4,7 @@ A customizable Discord bot that responds to @mentions with AI-generated response
## Features ## Features
- **Multi-Provider AI**: Supports OpenAI, OpenRouter, and Anthropic (Claude) - **Multi-Provider AI**: Supports OpenAI, OpenRouter, Anthropic (Claude), and Google Gemini
- **Fully Customizable**: Configure bot name, personality, and behavior - **Fully Customizable**: Configure bot name, personality, and behavior
- **Conversation Memory**: Remembers context per user - **Conversation Memory**: Remembers context per user
- **Easy Deployment**: Docker support included - **Easy Deployment**: Docker support included
@@ -50,10 +50,11 @@ All configuration is done via environment variables in `.env`.
| Variable | Description | | Variable | Description |
|----------|-------------| |----------|-------------|
| `DISCORD_TOKEN` | Your Discord bot token | | `DISCORD_TOKEN` | Your Discord bot token |
| `AI_PROVIDER` | `openai`, `openrouter`, or `anthropic` | | `AI_PROVIDER` | `openai`, `openrouter`, `anthropic`, or `gemini` |
| `OPENAI_API_KEY` | OpenAI API key (if using OpenAI) | | `OPENAI_API_KEY` | OpenAI API key (if using OpenAI) |
| `OPENROUTER_API_KEY` | OpenRouter API key (if using OpenRouter) | | `OPENROUTER_API_KEY` | OpenRouter API key (if using OpenRouter) |
| `ANTHROPIC_API_KEY` | Anthropic API key (if using Anthropic) | | `ANTHROPIC_API_KEY` | Anthropic API key (if using Anthropic) |
| `GEMINI_API_KEY` | Google Gemini API key (if using Gemini) |
### Bot Identity ### Bot Identity
@@ -129,6 +130,7 @@ Mention the bot in any channel:
| OpenAI | gpt-4o, gpt-4-turbo, gpt-3.5-turbo | Official OpenAI API | | OpenAI | gpt-4o, gpt-4-turbo, gpt-3.5-turbo | Official OpenAI API |
| OpenRouter | 100+ models | Access to Llama, Mistral, Claude, etc. | | OpenRouter | 100+ models | Access to Llama, Mistral, Claude, etc. |
| Anthropic | claude-3-5-sonnet, claude-3-opus | Direct Claude API | | Anthropic | claude-3-5-sonnet, claude-3-opus | Direct Claude API |
| Gemini | gemini-2.0-flash, gemini-1.5-pro | Google AI API |
## Project Structure ## Project Structure

View File

@@ -3,6 +3,7 @@ discord.py>=2.3.0
# AI Providers # AI Providers
anthropic>=0.18.0 anthropic>=0.18.0
google-genai>=1.0.0
openai>=1.12.0 openai>=1.12.0
# HTTP Client # HTTP Client

View File

@@ -19,7 +19,7 @@ class Settings(BaseSettings):
discord_token: str = Field(..., description="Discord bot token") discord_token: str = Field(..., description="Discord bot token")
# AI Provider Configuration # AI Provider Configuration
ai_provider: Literal["openai", "openrouter", "anthropic"] = Field( ai_provider: Literal["openai", "openrouter", "anthropic", "gemini"] = Field(
"openai", description="Which AI provider to use" "openai", description="Which AI provider to use"
) )
ai_model: str = Field("gpt-4o", description="AI model to use") ai_model: str = Field("gpt-4o", description="AI model to use")
@@ -30,6 +30,7 @@ class Settings(BaseSettings):
openai_api_key: str | None = Field(None, description="OpenAI API key") openai_api_key: str | None = Field(None, description="OpenAI API key")
openrouter_api_key: str | None = Field(None, description="OpenRouter API key") openrouter_api_key: str | None = Field(None, description="OpenRouter API key")
anthropic_api_key: str | None = Field(None, description="Anthropic API key") anthropic_api_key: str | None = Field(None, description="Anthropic API key")
gemini_api_key: str | None = Field(None, description="Google Gemini API key")
# Logging # Logging
log_level: str = Field("INFO", description="Logging level") log_level: str = Field("INFO", description="Logging level")
@@ -66,6 +67,7 @@ class Settings(BaseSettings):
"openai": self.openai_api_key, "openai": self.openai_api_key,
"openrouter": self.openrouter_api_key, "openrouter": self.openrouter_api_key,
"anthropic": self.anthropic_api_key, "anthropic": self.anthropic_api_key,
"gemini": self.gemini_api_key,
} }
key = key_map.get(self.ai_provider) key = key_map.get(self.ai_provider)
if not key: if not key:

View File

@@ -9,6 +9,7 @@ from .providers import (
AIProvider, AIProvider,
AIResponse, AIResponse,
AnthropicProvider, AnthropicProvider,
GeminiProvider,
Message, Message,
OpenAIProvider, OpenAIProvider,
OpenRouterProvider, OpenRouterProvider,
@@ -16,7 +17,7 @@ from .providers import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ProviderType = Literal["openai", "openrouter", "anthropic"] ProviderType = Literal["openai", "openrouter", "anthropic", "gemini"]
class AIService: class AIService:
@@ -45,6 +46,7 @@ class AIService:
"openai": OpenAIProvider, "openai": OpenAIProvider,
"openrouter": OpenRouterProvider, "openrouter": OpenRouterProvider,
"anthropic": AnthropicProvider, "anthropic": AnthropicProvider,
"gemini": GeminiProvider,
} }
provider_class = providers.get(provider_type) provider_class = providers.get(provider_type)

View File

@@ -2,6 +2,7 @@
from .anthropic import AnthropicProvider from .anthropic import AnthropicProvider
from .base import AIProvider, AIResponse, Message from .base import AIProvider, AIResponse, Message
from .gemini import GeminiProvider
from .openai import OpenAIProvider from .openai import OpenAIProvider
from .openrouter import OpenRouterProvider from .openrouter import OpenRouterProvider
@@ -12,4 +13,5 @@ __all__ = [
"OpenAIProvider", "OpenAIProvider",
"OpenRouterProvider", "OpenRouterProvider",
"AnthropicProvider", "AnthropicProvider",
"GeminiProvider",
] ]

View File

@@ -0,0 +1,71 @@
"""Google Gemini provider implementation."""
import logging
from google import genai
from google.genai import types
from .base import AIProvider, AIResponse, Message
logger = logging.getLogger(__name__)
class GeminiProvider(AIProvider):
"""Google Gemini API provider."""
def __init__(self, api_key: str, model: str = "gemini-2.0-flash") -> None:
self.client = genai.Client(api_key=api_key)
self.model = model
@property
def provider_name(self) -> str:
return "gemini"
async def generate(
self,
messages: list[Message],
system_prompt: str | None = None,
max_tokens: int = 1024,
temperature: float = 0.7,
) -> AIResponse:
"""Generate a response using Gemini."""
# Build contents list (Gemini format)
contents = []
for m in messages:
# Gemini uses "user" and "model" roles
role = "model" if m.role == "assistant" else m.role
contents.append(types.Content(role=role, parts=[types.Part(text=m.content)]))
logger.debug(f"Sending {len(contents)} messages to Gemini")
# Build config
config = types.GenerateContentConfig(
max_output_tokens=max_tokens,
temperature=temperature,
)
if system_prompt:
config.system_instruction = system_prompt
response = await self.client.aio.models.generate_content(
model=self.model,
contents=contents,
config=config,
)
# Extract text from response
content = response.text or ""
# Build usage dict
usage = {}
if response.usage_metadata:
usage = {
"prompt_tokens": response.usage_metadata.prompt_token_count or 0,
"completion_tokens": response.usage_metadata.candidates_token_count or 0,
"total_tokens": response.usage_metadata.total_token_count or 0,
}
return AIResponse(
content=content,
model=self.model,
usage=usage,
)