diff --git a/.env.example b/.env.example index e248e075..84b05306 100644 --- a/.env.example +++ b/.env.example @@ -85,7 +85,7 @@ BACKLOG_SYNC_BATCH_SIZE=100 # Messages per backlog batch, max 100 (d # === AI Analysis === AI_ANALYSIS_ENABLED=false # Enable AI content moderation (default: false) AI_LLM_API_KEY= # REQUIRED if AI_ANALYSIS_ENABLED=true. LLM API key -AI_LLM_BASE_URL=https://9router.asepharyana.my.id/v1 # LLM API base URL (9router — OpenAI-compatible router, replaces omniroute) +AI_LLM_BASE_URL=http://100.121.180.82:20128/api/v1 # LLM API base URL (omniroute — OpenAI-compatible router on imrnes, Tailscale 100.121.180.82) AI_LLM_MODEL=text # LLM text model name (default: text) # AI_LLM_VISION_MODEL= # Vision model for image analysis (falls back to AI_LLM_MODEL) # AI_LLM_EMBEDDING_MODEL= # Embedding model for semantic moderation cache (optional; enables near-duplicate text reuse to save LLM calls) diff --git a/services/backend/src/modules/chatbot/chatbot.service.ts b/services/backend/src/modules/chatbot/chatbot.service.ts index 791e43a8..b1e3dc27 100644 --- a/services/backend/src/modules/chatbot/chatbot.service.ts +++ b/services/backend/src/modules/chatbot/chatbot.service.ts @@ -158,7 +158,7 @@ Gaya ngobrol: tool_choice: "auto", max_tokens: 600, temperature: 0.4, - // Non-streaming: request a single complete response. 9router may + // Non-streaming: request a single complete response. omniroute may // still emit SSE even with stream:false, so the parser below // handle both raw-JSON and SSE bodies. stream: false, @@ -176,7 +176,7 @@ Gaya ngobrol: }, ); - // Parse the body into content + tool_calls. 9router may return either + // Parse the body into content + tool_calls. omniroute may return either // a single JSON object (stream:false honored) or SSE text (stream // implied) — parseResponse handles both. const { content, toolCalls } = this.parseResponse( @@ -252,7 +252,7 @@ Gaya ngobrol: /** * Parse an LLM HTTP body into content + tool_calls. Handles both shapes - * 9router can return: a single JSON object (stream:false honored) or SSE + * omniroute can return: a single JSON object (stream:false honored) or SSE * text (stream implied). For SSE we delegate to parseSse. */ private parseResponse(body: string): { @@ -306,7 +306,7 @@ Gaya ngobrol: /** * Parse an SSE stream body into accumulated content + any tool_calls. - * 9router (and most OpenAI-compatible routers) emit `data: {json}` lines + * omniroute (and most OpenAI-compatible routers) emit `data: {json}` lines * even when stream is only implied; we must collect deltas manually. */ private parseSse(body: string): { diff --git a/services/backend/src/shared/config/index.ts b/services/backend/src/shared/config/index.ts index 7a3cb3db..ee4f3c89 100644 --- a/services/backend/src/shared/config/index.ts +++ b/services/backend/src/shared/config/index.ts @@ -131,7 +131,7 @@ export const configSchema = z AI_LLM_BASE_URL: z .string() .url() - .default("https://9router.asepharyana.my.id/v1"), + .default("http://100.121.180.82:20128/api/v1"), AI_LLM_MODEL: z.string().default("text"), AI_LLM_VISION_MODEL: z.string().optional(), AI_LLM_EMBEDDING_MODEL: z.string().optional(), diff --git a/services/discord-gateway/ARCHITECTURE.md b/services/discord-gateway/ARCHITECTURE.md index 3cbb3d18..3d3b5c79 100644 --- a/services/discord-gateway/ARCHITECTURE.md +++ b/services/discord-gateway/ARCHITECTURE.md @@ -139,5 +139,5 @@ pipeline gauges — `ai_analysis_queued_conversations`, numeric snowflake IDs never trigger false positives. - **Semantic cache is batched** (one embed call + one Qdrant batch search), not N sequential round-trips. `ensureQdrantCollection` is memoized. -- **Streaming is mandatory** against the 9router base URL (non-stream waits for +- **Streaming is mandatory** against the omniroute base URL (non-stream waits for the full body and times out). `llmClient` aggregates SSE chunks. diff --git a/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts b/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts index 6839ddaa..d63b4f01 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmCaller.ts @@ -85,7 +85,7 @@ export async function callModerationLLM( jsonResponse: { type: "json_object" }, retries: 0, signal, - // Router (9router / formerly omniroute) always streams SSE even when the + // Router (omniroute) always streams SSE even when the // request omits `stream`. In non-stream mode the OpenAI SDK waits // for the FULL body before parsing, so slow/long upstream streams // hit the 30s/60s timeout and abort mid-generation. Streaming mode diff --git a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts index a9647e09..c87c6d9c 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts @@ -94,7 +94,7 @@ type LLMResponseChunk = { * `delta.content`; falls back to reasoning fields so reasoning-only models * still produce usable aggregated text. Providers differ in the field name: * - DeepSeek-style / Cloudflare gemma → `delta.reasoning_content` - * - mimo (via 9router) streams reasoning in `delta.reasoning` + + * - mimo (via omniroute) streams reasoning in `delta.reasoning` + * `delta.reasoning_details[].text` (content:"") — without these fallbacks * vision aggregation came back empty ("Vision API null response"). * Exported for unit tests. @@ -225,7 +225,7 @@ export function buildLlmParams( reasoning: { enabled: false }, // vLLM / Qwen / litellm chat_template_kwargs: { enable_thinking: false }, - // Anthropic / Claude-format (9router exposes thinkingFormat + // Anthropic / Claude-format (omniroute exposes thinkingFormat // "claude-adaptive" / "claude-budget" on its reasoning models) thinking: { type: "disabled" }, } as Record); diff --git a/services/discord-gateway/src/shared/config/index.ts b/services/discord-gateway/src/shared/config/index.ts index 2ad0b285..b8ead9b0 100644 --- a/services/discord-gateway/src/shared/config/index.ts +++ b/services/discord-gateway/src/shared/config/index.ts @@ -149,7 +149,7 @@ export const configSchema = z AI_LLM_BASE_URL: z .string() .url() - .default("https://9router.asepharyana.my.id/v1"), + .default("http://100.121.180.82:20128/api/v1"), AI_LLM_MODEL: z.string().default("text"), // Vision uses the SAME router/base URL as text moderation // (AI_LLM_BASE_URL) but a different model alias. The dedicated NVIDIA diff --git a/services/discord-gateway/tests/llmChunkExtraction.test.ts b/services/discord-gateway/tests/llmChunkExtraction.test.ts index 8b358351..9a56307a 100644 --- a/services/discord-gateway/tests/llmChunkExtraction.test.ts +++ b/services/discord-gateway/tests/llmChunkExtraction.test.ts @@ -1,7 +1,7 @@ // ═══════════════════════════════════════════════════════════════════════════ // llmClient chunk extraction — reasoning_content fallback (pure, no network) // ═══════════════════════════════════════════════════════════════════════════ -// Regression: 9router "multimodal" combo routed to cloudflare gemma-4-26b +// Regression: omniroute "multimodal" combo routed to cloudflare gemma-4-26b // which streams ALL output in delta.reasoning_content with content:"" — the // old extractor returned empty text → llmVision reported "Vision API null // response" → every image moderation batch fell back to text-only analysis @@ -19,7 +19,7 @@ describe("extractChunkText — streaming chunk text extraction", () => { }); it("falls back to delta.reasoning_content when content is empty — reasoning-only models (cloudflare gemma)", () => { - // Exact shape seen from 9router → cloudflare-ai/@cf/google/gemma-4-26b: + // Exact shape seen from omniroute → cloudflare-ai/@cf/google/gemma-4-26b: // {"choices":[{"delta":{"content":"","reasoning_content":"Task","role":"assistant"},"finish_reason":null,...}]} expect( extractChunkText({ @@ -33,8 +33,8 @@ describe("extractChunkText — streaming chunk text extraction", () => { ).toBe("Task"); }); - it('falls back to delta.reasoning — mimo via 9router streams reasoning there with content:""', () => { - // Exact shape seen from 9router → mimo-v2.5-free (2026-08-11): + it('falls back to delta.reasoning — mimo via omniroute streams reasoning there with content:""', () => { + // Exact shape seen from omniroute → mimo-v2.5-free (2026-08-11): // {"choices":[{"delta":{"content":"","reasoning":"The user wants a","role":"assistant"},"finish_reason":null,...}]} expect( extractChunkText({ diff --git a/services/discord-gateway/tests/visionNoImageSeen.test.ts b/services/discord-gateway/tests/visionNoImageSeen.test.ts index 9a08ad70..ff0f8790 100644 --- a/services/discord-gateway/tests/visionNoImageSeen.test.ts +++ b/services/discord-gateway/tests/visionNoImageSeen.test.ts @@ -6,7 +6,7 @@ // a VALID vision_llm result. Every later analysis of the same image (same // hash / phash) then hit the poisoned cache and the moderation LLM wrote // "lampiran yang gagal terbaca" — image analysis seemed permanently broken -// even though 9router was responding fine. +// even though omniroute was responding fine. import { describe, expect, it } from "vitest"; import { isNoImageSeenText } from "../src/modules/ai-moderation/visionAnalyzer.js";