mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-25 09:14:39 +02:00
✨ feat(chat,picture): add OpenRouter integration and credit system
Chat: - Add OpenRouter as primary AI provider with multiple models - Update chat service with new model configurations - Add model seed data for Llama, DeepSeek, Mistral, Claude, GPT-4o Picture: - Integrate @mana-core/nestjs-integration for credit system - Implement freemium model (3 free generations, then 10 credits) - Migrate storage to @manacore/shared-storage - Add comprehensive project documentation
This commit is contained in:
parent
422fcd6b34
commit
6f74e1d9a6
12 changed files with 878 additions and 468 deletions
|
|
@ -66,7 +66,7 @@ pnpm preview # Preview production build
|
|||
- **Mobile**: React Native 0.76.7 + Expo SDK 52, NativeWind, Expo Router
|
||||
- **Web**: SvelteKit 2.x, Svelte 5, Tailwind CSS 4
|
||||
- **Landing**: Astro 5.16, Tailwind CSS
|
||||
- **Backend**: NestJS 10, Google Gemini AI, Supabase
|
||||
- **Backend**: NestJS 10, OpenRouter/Gemini AI, Supabase
|
||||
- **Types**: TypeScript 5.x
|
||||
|
||||
## Architecture
|
||||
|
|
@ -89,11 +89,12 @@ pnpm preview # Preview production build
|
|||
#### Backend (.env)
|
||||
|
||||
```
|
||||
GOOGLE_GENAI_API_KEY=...
|
||||
OPENROUTER_API_KEY=... # Get at https://openrouter.ai/keys
|
||||
GOOGLE_GENAI_API_KEY=... # Optional: For Gemini models
|
||||
SUPABASE_URL=https://...
|
||||
SUPABASE_SERVICE_KEY=...
|
||||
PORT=3002
|
||||
DEV_BYPASS_AUTH=true # Optional: Skip auth in development
|
||||
DEV_BYPASS_AUTH=true # Optional: Skip auth in development
|
||||
```
|
||||
|
||||
#### Mobile (.env)
|
||||
|
|
@ -114,11 +115,33 @@ EXPO_PUBLIC_BACKEND_URL=http://localhost:3001
|
|||
|
||||
## AI Models Available
|
||||
|
||||
| Model ID | Name | Description | Default |
|
||||
| ------------------------------------ | --------------------- | ------------------------- | ------- |
|
||||
| 550e8400-e29b-41d4-a716-446655440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes |
|
||||
| 550e8400-e29b-41d4-a716-446655440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No |
|
||||
| 550e8400-e29b-41d4-a716-446655440103 | Gemini 2.5 Pro | Most capable model | No |
|
||||
### OpenRouter Models (Recommended)
|
||||
|
||||
| Model ID | Name | Price | Best For |
|
||||
| -------- | ---- | ----- | -------- |
|
||||
| ...440201 | Llama 3.1 8B | $0.05/M | Everyday tasks, cheap |
|
||||
| ...440202 | Llama 3.1 70B | $0.35/M | Complex reasoning |
|
||||
| ...440203 | DeepSeek V3 | $0.14/M | Reasoning at low cost |
|
||||
| ...440204 | Mistral Small | $0.10/M | General tasks |
|
||||
| ...440205 | Claude 3.5 Sonnet | $3/M | Best quality |
|
||||
| ...440206 | GPT-4o Mini | $0.15/M | Balanced performance |
|
||||
|
||||
### Google Gemini Models
|
||||
|
||||
| Model ID | Name | Description | Default |
|
||||
| -------- | ---- | ----------- | ------- |
|
||||
| ...440101 | Gemini 2.5 Flash | Fast, efficient responses | Yes |
|
||||
| ...440102 | Gemini 2.0 Flash-Lite | Ultra-lightweight model | No |
|
||||
| ...440103 | Gemini 2.5 Pro | Most capable model | No |
|
||||
|
||||
## OpenRouter Setup
|
||||
|
||||
To enable OpenRouter models:
|
||||
|
||||
- [ ] Get API key at https://openrouter.ai/keys
|
||||
- [ ] Add `OPENROUTER_API_KEY=sk-or-v1-xxx` to `apps/chat/apps/backend/.env`
|
||||
- [ ] Re-seed database: `pnpm --filter @chat/backend db:seed`
|
||||
- [ ] Test: `pnpm dev:chat:backend`
|
||||
|
||||
## Important Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
# Azure OpenAI Configuration
|
||||
# OpenRouter Configuration (Recommended - multi-model access)
|
||||
# Get your API key at https://openrouter.ai/keys
|
||||
OPENROUTER_API_KEY=your-openrouter-api-key
|
||||
|
||||
# Google Gemini Configuration
|
||||
GOOGLE_GENAI_API_KEY=your-google-api-key
|
||||
|
||||
# Azure OpenAI Configuration (Optional)
|
||||
AZURE_OPENAI_ENDPOINT=https://your-azure-openai-endpoint.openai.azure.com
|
||||
AZURE_OPENAI_API_KEY=your-api-key-here
|
||||
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { AsyncResult, ok, err, ValidationError, ServiceError } from '@manacore/shared-errors';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import OpenAI from 'openai';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { Database } from '../db/connection';
|
||||
import { models } from '../db/schema/models.schema';
|
||||
|
|
@ -19,6 +20,8 @@ export class ChatService {
|
|||
private readonly azureApiVersion: string;
|
||||
// Google Gemini config
|
||||
private readonly geminiClient: GoogleGenerativeAI | null = null;
|
||||
// OpenRouter config
|
||||
private readonly openRouterClient: OpenAI | null = null;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
|
|
@ -41,6 +44,22 @@ export class ChatService {
|
|||
this.logger.warn('GOOGLE_GENAI_API_KEY is not set - Gemini models unavailable');
|
||||
}
|
||||
|
||||
// OpenRouter setup
|
||||
const openRouterApiKey = this.configService.get<string>('OPENROUTER_API_KEY');
|
||||
if (openRouterApiKey) {
|
||||
this.openRouterClient = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: openRouterApiKey,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': this.configService.get<string>('APP_URL') || 'http://localhost:3002',
|
||||
'X-Title': 'Mana Chat',
|
||||
},
|
||||
});
|
||||
this.logger.log('OpenRouter client initialized');
|
||||
} else {
|
||||
this.logger.warn('OPENROUTER_API_KEY is not set - OpenRouter models unavailable');
|
||||
}
|
||||
|
||||
if (!this.azureApiKey) {
|
||||
this.logger.warn('AZURE_OPENAI_API_KEY is not set - Azure models unavailable');
|
||||
}
|
||||
|
|
@ -84,6 +103,8 @@ export class ChatService {
|
|||
// Route to appropriate provider
|
||||
if (model.provider === 'gemini') {
|
||||
return this.createGeminiCompletion(model, dto);
|
||||
} else if (model.provider === 'openrouter') {
|
||||
return this.createOpenRouterCompletion(model, dto);
|
||||
} else {
|
||||
return this.createAzureCompletion(model, dto);
|
||||
}
|
||||
|
|
@ -250,4 +271,62 @@ export class ChatService {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createOpenRouterCompletion(
|
||||
model: Model,
|
||||
dto: ChatCompletionDto
|
||||
): AsyncResult<ChatCompletionResponseDto> {
|
||||
if (!this.openRouterClient) {
|
||||
return err(ServiceError.externalError('OpenRouter', 'OpenRouter client not configured'));
|
||||
}
|
||||
|
||||
const params = model.parameters as {
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
} | null;
|
||||
|
||||
const modelName = params?.model || 'meta-llama/llama-3.1-8b-instruct';
|
||||
const temperature = dto.temperature ?? params?.temperature ?? 0.7;
|
||||
const maxTokens = dto.maxTokens ?? params?.max_tokens ?? 4096;
|
||||
|
||||
this.logger.log(`Sending request to OpenRouter model: ${modelName}`);
|
||||
|
||||
try {
|
||||
const response = await this.openRouterClient.chat.completions.create({
|
||||
model: modelName,
|
||||
messages: dto.messages.map((msg) => ({
|
||||
role: msg.role as 'system' | 'user' | 'assistant',
|
||||
content: msg.content,
|
||||
})),
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
|
||||
const messageContent = response.choices?.[0]?.message?.content;
|
||||
|
||||
if (!messageContent) {
|
||||
this.logger.warn('No message content in OpenRouter response');
|
||||
return err(ServiceError.generationFailed('OpenRouter', 'No response generated'));
|
||||
}
|
||||
|
||||
return ok({
|
||||
content: messageContent,
|
||||
usage: {
|
||||
prompt_tokens: response.usage?.prompt_tokens || 0,
|
||||
completion_tokens: response.usage?.completion_tokens || 0,
|
||||
total_tokens: response.usage?.total_tokens || 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Error calling OpenRouter API', error);
|
||||
return err(
|
||||
ServiceError.generationFailed(
|
||||
'OpenRouter',
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,87 @@ async function seed() {
|
|||
isDefault: false,
|
||||
},
|
||||
// ============================================
|
||||
// OpenRouter Models (Multi-provider, cost-effective)
|
||||
// ============================================
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440201',
|
||||
name: 'Llama 3.1 8B',
|
||||
description: 'Fast & cheap - great for everyday tasks ($0.05/M tokens)',
|
||||
provider: 'openrouter',
|
||||
parameters: {
|
||||
model: 'meta-llama/llama-3.1-8b-instruct',
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440202',
|
||||
name: 'Llama 3.1 70B',
|
||||
description: 'Powerful open model - complex reasoning ($0.35/M tokens)',
|
||||
provider: 'openrouter',
|
||||
parameters: {
|
||||
model: 'meta-llama/llama-3.1-70b-instruct',
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440203',
|
||||
name: 'DeepSeek V3',
|
||||
description: 'Excellent reasoning at low cost ($0.14/M tokens)',
|
||||
provider: 'openrouter',
|
||||
parameters: {
|
||||
model: 'deepseek/deepseek-chat',
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440204',
|
||||
name: 'Mistral Small',
|
||||
description: 'Fast European model - good for general tasks ($0.10/M tokens)',
|
||||
provider: 'openrouter',
|
||||
parameters: {
|
||||
model: 'mistralai/mistral-small-24b-instruct-2501',
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440205',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
description: 'Best overall quality - coding & analysis ($3/M tokens)',
|
||||
provider: 'openrouter',
|
||||
parameters: {
|
||||
model: 'anthropic/claude-3.5-sonnet',
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
id: '550e8400-e29b-41d4-a716-446655440206',
|
||||
name: 'GPT-4o Mini',
|
||||
description: 'OpenAI fast model - balanced performance ($0.15/M tokens)',
|
||||
provider: 'openrouter',
|
||||
parameters: {
|
||||
model: 'openai/gpt-4o-mini',
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
},
|
||||
// ============================================
|
||||
// Azure OpenAI GPT-5 Family (Inactive - no deployment)
|
||||
// ============================================
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue