diff --git a/services/discord-gateway/drizzle/migrations/0005_large_squadron_sinister.sql b/services/discord-gateway/drizzle/migrations/0005_large_squadron_sinister.sql index cf5a954..2a18484 100644 --- a/services/discord-gateway/drizzle/migrations/0005_large_squadron_sinister.sql +++ b/services/discord-gateway/drizzle/migrations/0005_large_squadron_sinister.sql @@ -1,11 +1,11 @@ -CREATE TABLE "channel_cultures" ( +CREATE TABLE IF NOT EXISTS "channel_cultures" ( "channel_id" text PRIMARY KEY NOT NULL, "guild_id" text NOT NULL, "culture_summary" text NOT NULL, "last_analyzed_at" bigint NOT NULL ); --> statement-breakpoint -CREATE TABLE "user_reputations" ( +CREATE TABLE IF NOT EXISTS "user_reputations" ( "user_id" text PRIMARY KEY NOT NULL, "guild_id" text NOT NULL, "trust_score" integer DEFAULT 50 NOT NULL, @@ -16,6 +16,6 @@ CREATE TABLE "user_reputations" ( "updated_at" bigint NOT NULL ); --> statement-breakpoint -CREATE INDEX "idx_channel_cultures_guild_id" ON "channel_cultures" USING btree ("guild_id");--> statement-breakpoint -CREATE INDEX "idx_user_reputations_guild_id" ON "user_reputations" USING btree ("guild_id");--> statement-breakpoint -CREATE INDEX "idx_user_reputations_trust_score" ON "user_reputations" USING btree ("trust_score"); \ No newline at end of file +CREATE INDEX IF NOT EXISTS "idx_channel_cultures_guild_id" ON "channel_cultures" USING btree ("guild_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_user_reputations_guild_id" ON "user_reputations" USING btree ("guild_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_user_reputations_trust_score" ON "user_reputations" USING btree ("trust_score"); \ No newline at end of file diff --git a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts index 1d0f67a..5c96ed6 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts @@ -33,12 +33,6 @@ function getClient(): OpenAI | null { return openaiClient; } -// --------------------------------------------------------------------------- -// Shared defaults -// --------------------------------------------------------------------------- - -const DEFAULT_TEMPERATURE = 0.2; -const DEFAULT_TOP_P = 0.95; const DEFAULT_RETRIES = 2; // --------------------------------------------------------------------------- @@ -60,6 +54,8 @@ export interface LlmCallOpts { jsonResponse?: { type: "json_object" }; /** Extra retries beyond DEFAULT_RETRIES (default 2). */ retries?: number; + /** Whether to use streaming (if true, will consume stream and return aggregated result) */ + stream?: boolean; } /** @@ -77,33 +73,74 @@ export async function llmChat( const { messages, model = config.AI_LLM_MODEL, - max_tokens = 8192, - temperature = DEFAULT_TEMPERATURE, - top_p = DEFAULT_TOP_P, + max_tokens, + temperature, + top_p, jsonResponse, retries = DEFAULT_RETRIES, + stream = false, } = opts; - const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = - { - model, - messages, - temperature, - top_p, - max_tokens, - stream: false, - }; + const params: any = { + model, + messages, + }; + + // Attach optional parameters only if explicitly provided to maintain + // maximum compatibility with various LLM providers and local APIs. + if (stream !== undefined) params.stream = stream; + if (temperature !== undefined) params.temperature = temperature; + if (top_p !== undefined) params.top_p = top_p; + if (max_tokens !== undefined) params.max_tokens = max_tokens; if (jsonResponse) { - (params as unknown as Record).response_format = - jsonResponse; + params.response_format = jsonResponse; } return retryWithBackoff( async () => { - return withLlmConcurrency(async () => - client.chat.completions.create(params), - ); + return withLlmConcurrency(async () => { + const response = await client.chat.completions.create(params); + if (stream) { + let content = ""; + let finishReason = "stop"; + for await (const chunk of response as any) { + const choice = chunk?.choices?.[0]; + + // Dynamic parsing to support multiple providers (OpenAI, Ollama, Groq, Anthropic via proxy, etc.) + const textChunk = + choice?.delta?.content || + choice?.message?.content || + choice?.text || + chunk?.message?.content || + chunk?.response || + chunk?.content || + ""; + + content += textChunk; + + const fr = choice?.finish_reason || chunk?.finish_reason; + if (fr) { + finishReason = fr; + } + } + return { + id: 'stream-aggregated', + choices: [ + { + message: { role: 'assistant', content, refusal: null }, + finish_reason: finishReason, + index: 0, + logprobs: null, + }, + ], + created: Math.floor(Date.now() / 1000), + model: model, + object: 'chat.completion', + } as OpenAI.Chat.Completions.ChatCompletion; + } + return response as OpenAI.Chat.Completions.ChatCompletion; + }); }, { retries,