v0.1.1: System default AI config, configurable URLs, single .env, website refactor
This commit is contained in:
@@ -1,51 +0,0 @@
|
||||
# =============================================================================
|
||||
# DearDiary Configuration
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database (SQLite, PostgreSQL, or MySQL)
|
||||
# -----------------------------------------------------------------------------
|
||||
DATABASE_URL="file:./data/deardiary.db"
|
||||
|
||||
# Example PostgreSQL:
|
||||
# DATABASE_URL="postgresql://postgres:password@db:5432/deardiary"
|
||||
|
||||
# Example MySQL:
|
||||
# DATABASE_URL="mysql://root:password@localhost:3306/deardiary"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Application
|
||||
# -----------------------------------------------------------------------------
|
||||
# App name displayed in UI
|
||||
APP_NAME="DearDiary"
|
||||
|
||||
# App version
|
||||
VERSION="0.1.0"
|
||||
|
||||
# Server port
|
||||
PORT="3000"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Security
|
||||
# -----------------------------------------------------------------------------
|
||||
# JWT secret for authentication tokens (REQUIRED in production)
|
||||
JWT_SECRET="change-this-to-a-random-string-in-production"
|
||||
|
||||
# CORS origin (use specific domain in production, e.g., "https://yourapp.com")
|
||||
CORS_ORIGIN="*"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# User Management
|
||||
# -----------------------------------------------------------------------------
|
||||
# Enable/disable user registration ("true" or "false")
|
||||
REGISTRATION_ENABLED="false"
|
||||
|
||||
# Default admin user (auto-created on startup if doesn't exist)
|
||||
DEFAULT_USER_EMAIL="admin@localhost"
|
||||
DEFAULT_USER_PASSWORD="changeme123"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Storage
|
||||
# -----------------------------------------------------------------------------
|
||||
# Media storage directory
|
||||
MEDIA_DIR="./data/media"
|
||||
@@ -4,7 +4,7 @@ generator client {
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
url = env("BACKEND_DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -94,16 +94,17 @@ model Task {
|
||||
}
|
||||
|
||||
model Settings {
|
||||
userId String @id
|
||||
aiProvider String @default("groq")
|
||||
aiApiKey String?
|
||||
aiModel String @default("llama-3.3-70b-versatile")
|
||||
aiBaseUrl String?
|
||||
journalPrompt String?
|
||||
language String @default("en")
|
||||
timezone String @default("UTC")
|
||||
providerSettings String?
|
||||
userId String @id
|
||||
aiProvider String @default("groq")
|
||||
aiApiKey String?
|
||||
aiModel String @default("llama-3.3-70b-versatile")
|
||||
aiBaseUrl String?
|
||||
journalPrompt String?
|
||||
language String @default("en")
|
||||
timezone String @default("UTC")
|
||||
providerSettings String?
|
||||
journalContextDays Int @default(10)
|
||||
useSystemDefault Boolean @default(true)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
@@ -8,19 +8,35 @@ import { createHash, randomBytes } from 'crypto';
|
||||
import * as jose from 'jose';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { createAIProvider } from './services/ai/provider';
|
||||
import { eventsRoutes } from './routes/events';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
const envVars = env(app);
|
||||
const envVars = env<{
|
||||
BACKEND_DATABASE_URL?: string;
|
||||
BACKEND_PORT?: string;
|
||||
BACKEND_JWT_SECRET?: string;
|
||||
BACKEND_CORS_ORIGIN?: string;
|
||||
BACKEND_REGISTRATION_ENABLED?: string;
|
||||
BACKEND_MEDIA_DIR?: string;
|
||||
BACKEND_DEFAULT_USER_EMAIL?: string;
|
||||
BACKEND_DEFAULT_USER_PASSWORD?: string;
|
||||
BACKEND_DEFAULT_AI_PROVIDER?: string;
|
||||
BACKEND_DEFAULT_AI_MODEL?: string;
|
||||
BACKEND_DEFAULT_AI_API_KEY?: string;
|
||||
BACKEND_DEFAULT_AI_BASE_URL?: string;
|
||||
BACKEND_VERSION?: string;
|
||||
BACKEND_APP_NAME?: string;
|
||||
}>(app);
|
||||
|
||||
app.use('*', logger());
|
||||
app.use('*', cors({
|
||||
origin: envVars.CORS_ORIGIN || '*',
|
||||
origin: envVars.BACKEND_CORS_ORIGIN || '*',
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
datasourceUrl: envVars.DATABASE_URL || 'file:./data/deardiary.db',
|
||||
datasourceUrl: envVars.BACKEND_DATABASE_URL || 'file:./data/deardiary.db',
|
||||
});
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
@@ -104,7 +120,7 @@ app.post('/api/v1/auth/login', async (c) => {
|
||||
return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } }, 401);
|
||||
}
|
||||
|
||||
const jwtSecret = envVars.JWT_SECRET || 'development-secret-change-in-production';
|
||||
const jwtSecret = envVars.BACKEND_JWT_SECRET || 'development-secret-change-in-production';
|
||||
const token = await new jose.SignJWT({ userId: user.id })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
@@ -117,9 +133,9 @@ app.post('/api/v1/auth/login', async (c) => {
|
||||
app.get('/api/v1/server-info', async (c) => {
|
||||
return c.json({
|
||||
data: {
|
||||
version: envVars.VERSION || '0.1.0',
|
||||
registrationEnabled: envVars.REGISTRATION_ENABLED !== 'false',
|
||||
appName: envVars.APP_NAME || 'DearDiary',
|
||||
version: envVars.BACKEND_VERSION || '0.1.0',
|
||||
registrationEnabled: envVars.BACKEND_REGISTRATION_ENABLED !== 'false',
|
||||
appName: envVars.BACKEND_APP_NAME || 'DearDiary',
|
||||
},
|
||||
error: null
|
||||
});
|
||||
@@ -132,7 +148,7 @@ app.post('/api/v1/auth/api-key', async (c) => {
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
const jwtSecret = envVars.JWT_SECRET || 'development-secret-change-in-production';
|
||||
const jwtSecret = envVars.BACKEND_JWT_SECRET || 'development-secret-change-in-production';
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
@@ -293,22 +309,7 @@ app.get('/api/v1/search', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/v1/events/:id', async (c) => {
|
||||
const userId = await getUserId(c);
|
||||
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
||||
|
||||
const { id } = c.req.param();
|
||||
const existing = await prisma.event.findFirst({ where: { id, userId } });
|
||||
if (!existing) return c.json({ data: null, error: { code: 'NOT_FOUND', message: 'Event not found' } }, 404);
|
||||
|
||||
const journal = await prisma.journal.findFirst({ where: { userId, date: existing.date } });
|
||||
if (journal) {
|
||||
return c.json({ data: null, error: { code: 'EVENT_IMMUTABLE', message: 'Cannot delete event: journal already generated. Delete the journal first.' } }, 400);
|
||||
}
|
||||
|
||||
await prisma.event.delete({ where: { id } });
|
||||
return c.json({ data: { deleted: true }, error: null });
|
||||
});
|
||||
app.route('/api/v1', eventsRoutes);
|
||||
|
||||
// Journal routes
|
||||
app.post('/api/v1/journal/generate/:date', async (c) => {
|
||||
@@ -333,7 +334,7 @@ app.post('/api/v1/journal/generate/:date', async (c) => {
|
||||
const providerConfig = providerSettings[provider] || {};
|
||||
const apiKey = providerConfig.apiKey || settings?.aiApiKey;
|
||||
|
||||
if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq') && !apiKey) {
|
||||
if ((provider === 'openai' || provider === 'anthropic' || provider === 'groq' || provider === 'xai') && !apiKey) {
|
||||
return c.json({ data: null, error: { code: 'NO_AI_CONFIG', message: 'AI not configured. Please set up your API key in settings.' } }, 400);
|
||||
}
|
||||
|
||||
@@ -602,15 +603,28 @@ app.get('/api/v1/journals', async (c) => {
|
||||
});
|
||||
});
|
||||
|
||||
const getDefaultSettings = () => ({
|
||||
aiProvider: envVars.BACKEND_DEFAULT_AI_PROVIDER || 'groq',
|
||||
aiApiKey: envVars.BACKEND_DEFAULT_AI_API_KEY || null,
|
||||
aiModel: envVars.BACKEND_DEFAULT_AI_MODEL || 'llama-3.3-70b-versatile',
|
||||
aiBaseUrl: envVars.BACKEND_DEFAULT_AI_BASE_URL || null,
|
||||
useSystemDefault: true,
|
||||
});
|
||||
|
||||
const getUserSettings = async (userId: string) => {
|
||||
let settings = await prisma.settings.findUnique({ where: { userId } });
|
||||
if (!settings) {
|
||||
settings = await prisma.settings.create({ data: { userId, ...getDefaultSettings() } });
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
// Settings routes
|
||||
app.get('/api/v1/settings', async (c) => {
|
||||
const userId = await getUserId(c);
|
||||
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
||||
|
||||
let settings = await prisma.settings.findUnique({ where: { userId } });
|
||||
if (!settings) {
|
||||
settings = await prisma.settings.create({ data: { userId } });
|
||||
}
|
||||
let settings = await getUserSettings(userId);
|
||||
|
||||
const oldDefaultLength = 400;
|
||||
if (settings.journalPrompt && settings.journalPrompt.length > oldDefaultLength) {
|
||||
@@ -625,21 +639,33 @@ app.put('/api/v1/settings', async (c) => {
|
||||
if (!userId) return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
||||
|
||||
const body = await c.req.json();
|
||||
const { aiProvider, aiApiKey, aiModel, aiBaseUrl, journalPrompt, language, providerSettings, journalContextDays } = body;
|
||||
const { aiProvider, aiApiKey, aiModel, aiBaseUrl, journalPrompt, language, providerSettings, journalContextDays, useSystemDefault } = body;
|
||||
|
||||
const defaults = getDefaultSettings();
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (aiProvider !== undefined) data.aiProvider = aiProvider;
|
||||
if (aiApiKey !== undefined) data.aiApiKey = aiApiKey;
|
||||
if (aiApiKey !== undefined) data.aiApiKey = aiApiKey || null;
|
||||
if (aiModel !== undefined) data.aiModel = aiModel;
|
||||
if (aiBaseUrl !== undefined) data.aiBaseUrl = aiBaseUrl;
|
||||
if (aiBaseUrl !== undefined) data.aiBaseUrl = aiBaseUrl || null;
|
||||
if (journalPrompt !== undefined) data.journalPrompt = journalPrompt === '' ? null : journalPrompt;
|
||||
if (language !== undefined) data.language = language;
|
||||
if (providerSettings !== undefined) data.providerSettings = JSON.stringify(providerSettings);
|
||||
if (journalContextDays !== undefined) data.journalContextDays = journalContextDays;
|
||||
if (useSystemDefault !== undefined) data.useSystemDefault = useSystemDefault;
|
||||
|
||||
const shouldUseSystemDefaults = useSystemDefault === true;
|
||||
|
||||
if (shouldUseSystemDefaults) {
|
||||
if (aiProvider === undefined) data.aiProvider = defaults.aiProvider;
|
||||
if (aiApiKey === undefined) data.aiApiKey = defaults.aiApiKey;
|
||||
if (aiModel === undefined) data.aiModel = defaults.aiModel;
|
||||
if (aiBaseUrl === undefined) data.aiBaseUrl = defaults.aiBaseUrl;
|
||||
}
|
||||
|
||||
const settings = await prisma.settings.upsert({
|
||||
where: { userId },
|
||||
create: { userId, ...data },
|
||||
create: { userId, ...getDefaultSettings(), ...data },
|
||||
update: data,
|
||||
});
|
||||
|
||||
@@ -1030,10 +1056,7 @@ app.post('/api/v1/account/reset', async (c) => {
|
||||
await prisma.settings.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
aiProvider: 'groq',
|
||||
aiApiKey: null,
|
||||
aiModel: 'llama-3.3-70b-versatile',
|
||||
aiBaseUrl: null,
|
||||
...getDefaultSettings(),
|
||||
journalPrompt: null,
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
@@ -1089,7 +1112,7 @@ app.post('/api/v1/account/password', async (c) => {
|
||||
});
|
||||
|
||||
app.post('/api/v1/auth/register', async (c) => {
|
||||
const registrationEnabled = envVars.REGISTRATION_ENABLED !== 'false';
|
||||
const registrationEnabled = envVars.BACKEND_REGISTRATION_ENABLED !== 'false';
|
||||
if (!registrationEnabled) {
|
||||
return c.json({ data: null, error: { code: 'REGISTRATION_DISABLED', message: 'Registration is currently disabled' } }, 403);
|
||||
}
|
||||
@@ -1184,11 +1207,11 @@ async function rebuildFTS() {
|
||||
}
|
||||
|
||||
async function createDefaultUser() {
|
||||
const defaultEmail = envVars.DEFAULT_USER_EMAIL;
|
||||
const defaultPassword = envVars.DEFAULT_USER_PASSWORD;
|
||||
const defaultEmail = envVars.BACKEND_DEFAULT_USER_EMAIL;
|
||||
const defaultPassword = envVars.BACKEND_DEFAULT_USER_PASSWORD;
|
||||
|
||||
if (!defaultEmail || !defaultPassword) {
|
||||
console.log('No default user configured (set DEFAULT_USER_EMAIL and DEFAULT_USER_PASSWORD)');
|
||||
console.log('No default user configured (set BACKEND_DEFAULT_USER_EMAIL and BACKEND_DEFAULT_USER_PASSWORD)');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1232,7 +1255,7 @@ async function createDefaultUser() {
|
||||
}
|
||||
}
|
||||
|
||||
const port = parseInt(envVars.PORT || '3000', 10);
|
||||
const port = parseInt(envVars.BACKEND_PORT || '3000', 10);
|
||||
console.log(`Starting DearDiary API on port ${port}`);
|
||||
|
||||
setupFTS().then(() => {
|
||||
|
||||
@@ -6,10 +6,19 @@ export interface HonoEnv {
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
Bindings: {
|
||||
DATABASE_URL: string;
|
||||
JWT_SECRET: string;
|
||||
MEDIA_DIR: string;
|
||||
PORT: string;
|
||||
CORS_ORIGIN: string;
|
||||
BACKEND_DATABASE_URL: string;
|
||||
BACKEND_JWT_SECRET: string;
|
||||
BACKEND_MEDIA_DIR: string;
|
||||
BACKEND_PORT: string;
|
||||
BACKEND_CORS_ORIGIN: string;
|
||||
BACKEND_REGISTRATION_ENABLED: string;
|
||||
BACKEND_DEFAULT_USER_EMAIL: string;
|
||||
BACKEND_DEFAULT_USER_PASSWORD: string;
|
||||
BACKEND_DEFAULT_AI_PROVIDER: string;
|
||||
BACKEND_DEFAULT_AI_MODEL: string;
|
||||
BACKEND_DEFAULT_AI_API_KEY: string;
|
||||
BACKEND_DEFAULT_AI_BASE_URL: string;
|
||||
BACKEND_VERSION: string;
|
||||
BACKEND_APP_NAME: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,32 @@ export async function authMiddleware(c: Context<HonoEnv>, next: Next) {
|
||||
|
||||
export function authenticate() {
|
||||
return async (c: Context<HonoEnv>, next: Next) => {
|
||||
await authMiddleware(c, next);
|
||||
const authHeader = c.req.header('Authorization');
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Missing or invalid Authorization header' } }, 401);
|
||||
}
|
||||
|
||||
const apiKey = authHeader.slice(7);
|
||||
const keyHash = createHash('sha256').update(apiKey).digest('hex');
|
||||
|
||||
const prisma = c.get('prisma');
|
||||
|
||||
const keyRecord = await prisma.apiKey.findUnique({
|
||||
where: { keyHash },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!keyRecord) {
|
||||
return c.json({ data: null, error: { code: 'UNAUTHORIZED', message: 'Invalid API key' } }, 401);
|
||||
}
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { id: keyRecord.id },
|
||||
data: { lastUsedAt: new Date() },
|
||||
});
|
||||
|
||||
c.set('userId', keyRecord.userId);
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ authRoutes.post('/login', async (c) => {
|
||||
}
|
||||
|
||||
const prisma = c.get('prisma');
|
||||
const jwtSecret = c.env.JWT_SECRET || 'development-secret-change-in-production';
|
||||
const jwtSecret = c.env.BACKEND_JWT_SECRET || 'development-secret-change-in-production';
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
@@ -88,7 +88,7 @@ authRoutes.post('/api-key', async (c) => {
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
const jwtSecret = c.env.JWT_SECRET || 'development-secret-change-in-production';
|
||||
const jwtSecret = c.env.BACKEND_JWT_SECRET || 'development-secret-change-in-production';
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Hono } from 'hono';
|
||||
import { HonoEnv } from '../lib/types';
|
||||
import { authenticate } from '../middleware/auth';
|
||||
|
||||
export const eventsRoutes = new Hono<HonoEnv>();
|
||||
|
||||
eventsRoutes.post('/', async (c) => {
|
||||
eventsRoutes.use(authenticate());
|
||||
|
||||
eventsRoutes.post('/events', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const prisma = c.get('prisma');
|
||||
const mediaDir = c.env.MEDIA_DIR || './data/media';
|
||||
const mediaDir = c.env.BACKEND_MEDIA_DIR || './data/media';
|
||||
|
||||
const body = await c.req.json();
|
||||
const { date, type, content, metadata, latitude, longitude, placeName } = body;
|
||||
@@ -42,7 +45,7 @@ eventsRoutes.post('/', async (c) => {
|
||||
return c.json({ data: event, error: null }, 201);
|
||||
});
|
||||
|
||||
eventsRoutes.get('/:id', async (c) => {
|
||||
eventsRoutes.get('/events/:id', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { id } = c.req.param();
|
||||
const prisma = c.get('prisma');
|
||||
@@ -58,7 +61,7 @@ eventsRoutes.get('/:id', async (c) => {
|
||||
return c.json({ data: event, error: null });
|
||||
});
|
||||
|
||||
eventsRoutes.put('/:id', async (c) => {
|
||||
eventsRoutes.put('/events/:id', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { id } = c.req.param();
|
||||
const prisma = c.get('prisma');
|
||||
@@ -97,7 +100,7 @@ eventsRoutes.put('/:id', async (c) => {
|
||||
return c.json({ data: event, error: null });
|
||||
});
|
||||
|
||||
eventsRoutes.delete('/:id', async (c) => {
|
||||
eventsRoutes.delete('/events/:id', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { id } = c.req.param();
|
||||
const prisma = c.get('prisma');
|
||||
@@ -120,11 +123,11 @@ eventsRoutes.delete('/:id', async (c) => {
|
||||
return c.json({ data: { deleted: true }, error: null });
|
||||
});
|
||||
|
||||
eventsRoutes.post('/:id/photo', async (c) => {
|
||||
eventsRoutes.post('/events/:id/photo', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { id } = c.req.param();
|
||||
const prisma = c.get('prisma');
|
||||
const mediaDir = c.env.MEDIA_DIR || './data/media';
|
||||
const mediaDir = c.env.BACKEND_MEDIA_DIR || './data/media';
|
||||
|
||||
const event = await prisma.event.findFirst({
|
||||
where: { id, userId },
|
||||
@@ -157,11 +160,11 @@ eventsRoutes.post('/:id/photo', async (c) => {
|
||||
return c.json({ data: { mediaPath: mediaUrl }, error: null }, 201);
|
||||
});
|
||||
|
||||
eventsRoutes.post('/:id/voice', async (c) => {
|
||||
eventsRoutes.post('/events/:id/voice', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const { id } = c.req.param();
|
||||
const prisma = c.get('prisma');
|
||||
const mediaDir = c.env.MEDIA_DIR || './data/media';
|
||||
const mediaDir = c.env.BACKEND_MEDIA_DIR || './data/media';
|
||||
|
||||
const event = await prisma.event.findFirst({
|
||||
where: { id, userId },
|
||||
|
||||
@@ -40,6 +40,7 @@ import { AnthropicProvider } from './anthropic';
|
||||
import { OllamaProvider } from './ollama';
|
||||
import { LMStudioProvider } from './lmstudio';
|
||||
import { GroqProvider } from './groq';
|
||||
import { XAIProvider } from './xai';
|
||||
import { CustomProvider } from './custom';
|
||||
|
||||
export function createAIProvider(config: AIProviderConfig): AIProvider {
|
||||
@@ -56,6 +57,8 @@ export function createAIProvider(config: AIProviderConfig): AIProvider {
|
||||
return new LMStudioProvider(config, settings);
|
||||
case 'groq':
|
||||
return new GroqProvider(config, settings);
|
||||
case 'xai':
|
||||
return new XAIProvider(config, settings);
|
||||
case 'custom':
|
||||
return new CustomProvider(config, settings);
|
||||
default:
|
||||
|
||||
92
backend/src/services/ai/xai.ts
Normal file
92
backend/src/services/ai/xai.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { AIProvider, AIProviderConfig, AIProviderResult } from './provider';
|
||||
import { ProviderSettings } from './openai';
|
||||
|
||||
export class XAIProvider implements AIProvider {
|
||||
provider = 'xai' as const;
|
||||
private apiKey: string;
|
||||
private model: string;
|
||||
private baseUrl: string;
|
||||
private customHeaders?: Record<string, string>;
|
||||
private customParameters?: Record<string, unknown>;
|
||||
|
||||
constructor(config: AIProviderConfig, settings?: ProviderSettings) {
|
||||
this.apiKey = config.apiKey || '';
|
||||
this.model = config.model || 'grok-4-1-fast-reasoning';
|
||||
this.baseUrl = config.baseUrl || 'https://api.x.ai/v1';
|
||||
this.customHeaders = settings?.headers;
|
||||
this.customParameters = settings?.parameters;
|
||||
}
|
||||
|
||||
async generate(prompt: string, systemPrompt?: string, options?: { jsonMode?: boolean }): Promise<AIProviderResult> {
|
||||
const messages: Array<{ role: string; content: string }> = [];
|
||||
|
||||
if (systemPrompt) {
|
||||
messages.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
|
||||
messages.push({ role: 'user', content: prompt });
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
max_tokens: 2000,
|
||||
...this.customParameters,
|
||||
};
|
||||
|
||||
if (options?.jsonMode) {
|
||||
requestBody.response_format = { type: 'json_object' };
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
...this.customHeaders,
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`xAI API error: ${response.status} ${JSON.stringify(responseData)}`);
|
||||
}
|
||||
|
||||
let content = responseData.choices?.[0]?.message?.content || '';
|
||||
let title: string | undefined;
|
||||
|
||||
if (options?.jsonMode) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
title = parsed.title;
|
||||
content = parsed.content || content;
|
||||
} catch {
|
||||
// If JSON parsing fails, use content as-is
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
title,
|
||||
request: requestBody,
|
||||
response: responseData,
|
||||
};
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
...this.customHeaders,
|
||||
};
|
||||
const response = await fetch(`${this.baseUrl}/models`, { headers });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user