feat(server): redis, config kv integration
This commit is contained in:
+1
-2
@@ -1,4 +1,5 @@
|
||||
DATABASE_URL="postgresql://postgres:example-PAssw0rd-xHjDYR.b7N@db:5432/postgres"
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
AUTH_GOOGLE_CLIENT_ID="change-me"
|
||||
AUTH_GOOGLE_CLIENT_SECRET="change-me"
|
||||
@@ -14,5 +15,3 @@ BACKEND_LLM_BASE_URL="change-me"
|
||||
|
||||
CLIENT_URL="change-me"
|
||||
|
||||
FLUX_PER_CENT=1
|
||||
FLUX_PER_REQUEST=1
|
||||
|
||||
@@ -18,6 +18,18 @@ services:
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
server:
|
||||
build:
|
||||
context: ../..
|
||||
@@ -25,6 +37,8 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
@@ -41,3 +55,5 @@ services:
|
||||
volumes:
|
||||
db_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"drizzle-valibot": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"injeca": "catalog:",
|
||||
"ioredis": "^5.6.1",
|
||||
"pg": "^8.20.0",
|
||||
"postgres": "^3.4.8",
|
||||
"stripe": "^20.3.0",
|
||||
|
||||
+26
-7
@@ -14,7 +14,7 @@ import { createLoggLogger, injeca, lifecycle } from 'injeca'
|
||||
import { createAuth } from './libs/auth'
|
||||
import { createDrizzle, migrateDatabase } from './libs/db'
|
||||
import { parsedEnv } from './libs/env'
|
||||
import { initOtel } from './libs/otel'
|
||||
import { createRedis } from './libs/redis'
|
||||
import { sessionMiddleware } from './middlewares/auth'
|
||||
import { otelMiddleware } from './middlewares/otel'
|
||||
import { createCharacterRoutes } from './routes/characters'
|
||||
@@ -25,6 +25,7 @@ import { createStripeRoutes } from './routes/stripe'
|
||||
import { createV1CompletionsRoutes } from './routes/v1completions'
|
||||
import { createCharacterService } from './services/characters'
|
||||
import { createChatService } from './services/chats'
|
||||
import { createConfigKVService } from './services/config-kv'
|
||||
import { createFluxService } from './services/flux'
|
||||
import { createProviderService } from './services/providers'
|
||||
import { ApiError, createInternalError } from './utils/error'
|
||||
@@ -35,6 +36,7 @@ type CharacterService = ReturnType<typeof createCharacterService>
|
||||
type ChatService = ReturnType<typeof createChatService>
|
||||
type ProviderService = ReturnType<typeof createProviderService>
|
||||
type FluxService = ReturnType<typeof createFluxService>
|
||||
type ConfigKVService = ReturnType<typeof createConfigKVService>
|
||||
|
||||
type OtelMetrics = ReturnType<typeof initOtel>
|
||||
|
||||
@@ -44,10 +46,11 @@ interface AppDeps {
|
||||
chatService: ChatService
|
||||
providerService: ProviderService
|
||||
fluxService: FluxService
|
||||
configKV: ConfigKVService
|
||||
env: Env
|
||||
}
|
||||
|
||||
function buildApp({ auth, characterService, chatService, providerService, fluxService, env }: AppDeps) {
|
||||
function buildApp({ auth, characterService, chatService, providerService, fluxService, configKV, env }: AppDeps) {
|
||||
const logger = useLogger('app').useGlobalConfig()
|
||||
|
||||
const app = new Hono<HonoEnv>()
|
||||
@@ -115,7 +118,7 @@ function buildApp({ auth, characterService, chatService, providerService, fluxSe
|
||||
/**
|
||||
* V1 routes for official provider.
|
||||
*/
|
||||
.route('/v1', createV1CompletionsRoutes(fluxService, env))
|
||||
.route('/v1', createV1CompletionsRoutes(fluxService, configKV, env))
|
||||
|
||||
/**
|
||||
* Flux routes.
|
||||
@@ -125,7 +128,7 @@ function buildApp({ auth, characterService, chatService, providerService, fluxSe
|
||||
/**
|
||||
* Stripe routes.
|
||||
*/
|
||||
.route('/api/stripe', createStripeRoutes(fluxService, env))
|
||||
.route('/api/stripe', createStripeRoutes(fluxService, configKV, env))
|
||||
}
|
||||
|
||||
export type AppType = ReturnType<typeof buildApp>
|
||||
@@ -181,19 +184,35 @@ async function createApp() {
|
||||
build: ({ dependsOn }) => createChatService(dependsOn.db),
|
||||
})
|
||||
|
||||
const redis = injeca.provide('services:redis', {
|
||||
dependsOn: { env: parsedEnv },
|
||||
build: async ({ dependsOn }) => {
|
||||
const redisInstance = createRedis(dependsOn.env.REDIS_URL)
|
||||
await redisInstance.connect()
|
||||
logger.log('Connected to Redis')
|
||||
return redisInstance
|
||||
},
|
||||
})
|
||||
|
||||
const configKV = injeca.provide('services:configKV', {
|
||||
dependsOn: { redis },
|
||||
build: ({ dependsOn }) => createConfigKVService(dependsOn.redis),
|
||||
})
|
||||
|
||||
const fluxService = injeca.provide('services:flux', {
|
||||
dependsOn: { db },
|
||||
build: ({ dependsOn }) => createFluxService(dependsOn.db),
|
||||
dependsOn: { db, configKV },
|
||||
build: ({ dependsOn }) => createFluxService(dependsOn.db, dependsOn.configKV),
|
||||
})
|
||||
|
||||
await injeca.start()
|
||||
const resolved = await injeca.resolve({ auth, characterService, chatService, providerService, fluxService, env: parsedEnv })
|
||||
const resolved = await injeca.resolve({ auth, characterService, chatService, providerService, fluxService, configKV, env: parsedEnv })
|
||||
const app = buildApp({
|
||||
auth: resolved.auth,
|
||||
characterService: resolved.characterService,
|
||||
chatService: resolved.chatService,
|
||||
providerService: resolved.providerService,
|
||||
fluxService: resolved.fluxService,
|
||||
configKV: resolved.configKV,
|
||||
env: resolved.env,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import { env, exit } from 'node:process'
|
||||
|
||||
import { useLogger } from '@guiiai/logg'
|
||||
import { injeca } from 'injeca'
|
||||
import { nonEmpty, object, optional, parse, pipe, string, transform } from 'valibot'
|
||||
import { nonEmpty, object, optional, parse, pipe, string } from 'valibot'
|
||||
|
||||
const EnvSchema = object({
|
||||
API_SERVER_URL: optional(string(), 'http://localhost:3000'),
|
||||
CLIENT_URL: optional(string(), 'https://airi.moerui.ai'),
|
||||
|
||||
DATABASE_URL: pipe(string(), nonEmpty('DATABASE_URL is required')),
|
||||
REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')),
|
||||
|
||||
AUTH_GOOGLE_CLIENT_ID: pipe(string(), nonEmpty('AUTH_GOOGLE_CLIENT_ID is required')),
|
||||
AUTH_GOOGLE_CLIENT_SECRET: pipe(string(), nonEmpty('AUTH_GOOGLE_CLIENT_SECRET is required')),
|
||||
@@ -19,9 +21,6 @@ const EnvSchema = object({
|
||||
STRIPE_SECRET_KEY: optional(string()),
|
||||
STRIPE_WEBHOOK_SECRET: optional(string()),
|
||||
|
||||
FLUX_PER_CENT: optional(pipe(string(), transform(Number)), '1'),
|
||||
FLUX_PER_REQUEST: optional(pipe(string(), transform(Number)), '1'),
|
||||
|
||||
BACKEND_LLM_BASE_URL: optional(string()),
|
||||
BACKEND_LLM_API_KEY: optional(string()),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import Redis from 'ioredis'
|
||||
|
||||
export function createRedis(url: string): Redis {
|
||||
return new Redis(url, { lazyConnect: true })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Context } from 'hono'
|
||||
|
||||
import type { Env } from '../libs/env'
|
||||
import type { ConfigKVService } from '../services/config-kv'
|
||||
import type { FluxService } from '../services/flux'
|
||||
import type { HonoEnv } from '../types/hono'
|
||||
|
||||
@@ -17,7 +18,7 @@ const SAFE_RESPONSE_HEADERS = new Set([
|
||||
'cache-control',
|
||||
])
|
||||
|
||||
export function createV1CompletionsRoutes(fluxService: FluxService, env: Env) {
|
||||
export function createV1CompletionsRoutes(fluxService: FluxService, configKV: ConfigKVService, env: Env) {
|
||||
async function handleCompletion(c: Context<HonoEnv>) {
|
||||
const user = c.get('user')!
|
||||
const flux = await fluxService.getFlux(user.id)
|
||||
@@ -27,7 +28,8 @@ export function createV1CompletionsRoutes(fluxService: FluxService, env: Env) {
|
||||
|
||||
const body = await c.req.json()
|
||||
|
||||
await fluxService.consumeFlux(user.id, env.FLUX_PER_REQUEST)
|
||||
const fluxPerRequest = await configKV.get('FLUX_PER_REQUEST')
|
||||
await fluxService.consumeFlux(user.id, fluxPerRequest)
|
||||
|
||||
const response = await fetch(`${env.BACKEND_LLM_BASE_URL}chat/completions`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type Redis from 'ioredis'
|
||||
|
||||
import { createServiceUnavailableError } from '../utils/error'
|
||||
|
||||
interface ConfigDefinitions {
|
||||
FLUX_PER_CENT: number
|
||||
FLUX_PER_REQUEST: number
|
||||
INITIAL_USER_FLUX: number
|
||||
}
|
||||
|
||||
const KEY_PREFIX = 'config:'
|
||||
|
||||
export function createConfigKVService(redis: Redis) {
|
||||
return {
|
||||
async get<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
|
||||
const raw = await redis.get(`${KEY_PREFIX}${key}`)
|
||||
if (raw === null)
|
||||
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
|
||||
|
||||
return Number(raw) as ConfigDefinitions[K]
|
||||
},
|
||||
|
||||
async set<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): Promise<void> {
|
||||
await redis.set(`${KEY_PREFIX}${key}`, String(value))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type ConfigKVService = ReturnType<typeof createConfigKVService>
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Database } from '../libs/db'
|
||||
import type { ConfigKVService } from './config-kv'
|
||||
|
||||
import { and, eq, gte, sql } from 'drizzle-orm'
|
||||
|
||||
@@ -6,7 +7,7 @@ import { createPaymentRequiredError } from '../utils/error'
|
||||
|
||||
import * as schema from '../schemas/flux'
|
||||
|
||||
export function createFluxService(db: Database) {
|
||||
export function createFluxService(db: Database, configKV: ConfigKVService) {
|
||||
return {
|
||||
async getFlux(userId: string) {
|
||||
let record = await db.query.userFlux.findFirst({
|
||||
@@ -14,9 +15,10 @@ export function createFluxService(db: Database) {
|
||||
})
|
||||
|
||||
if (!record) {
|
||||
[record] = await db.insert(schema.userFlux).values({
|
||||
const initialFlux = await configKV.get('INITIAL_USER_FLUX')
|
||||
;[record] = await db.insert(schema.userFlux).values({
|
||||
userId,
|
||||
flux: 100, // Default initial flux
|
||||
flux: initialFlux,
|
||||
}).returning()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user