Files
worldmonitor/server/_shared/llm.ts
Elie Habib 2a7d7fc3fe fix: standardize brand name to "World Monitor" with space (#1463)
Replace "WorldMonitor" with "World Monitor" in all user-facing display
text across blog posts, docs, layouts, structured data, footer, offline
page, and X-Title headers. Technical identifiers (User-Agent strings,
X-WorldMonitor-Key headers, @WorldMonitorApp handle, function names)
are preserved unchanged. Also adds anchors color to Mintlify docs config
to fix blue link color in dark mode.
2026-03-12 01:28:16 +04:00

195 lines
5.2 KiB
TypeScript

import { CHROME_UA } from './constants';
export interface ProviderCredentials {
apiUrl: string;
model: string;
headers: Record<string, string>;
extraBody?: Record<string, unknown>;
}
const OLLAMA_HOST_ALLOWLIST = new Set([
'localhost', '127.0.0.1', '::1', '[::1]', 'host.docker.internal',
]);
function isSidecar(): boolean {
return typeof process !== 'undefined' &&
(process.env?.LOCAL_API_MODE || '').includes('sidecar');
}
export function getProviderCredentials(provider: string): ProviderCredentials | null {
if (provider === 'ollama') {
const baseUrl = process.env.OLLAMA_API_URL;
if (!baseUrl) return null;
if (!isSidecar()) {
try {
const hostname = new URL(baseUrl).hostname;
if (!OLLAMA_HOST_ALLOWLIST.has(hostname)) {
console.warn(`[llm] Ollama blocked: hostname "${hostname}" not in allowlist`);
return null;
}
} catch {
return null;
}
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
const apiKey = process.env.OLLAMA_API_KEY;
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
return {
apiUrl: new URL('/v1/chat/completions', baseUrl).toString(),
model: process.env.OLLAMA_MODEL || 'llama3.1:8b',
headers,
extraBody: { think: false },
};
}
if (provider === 'groq') {
const apiKey = process.env.GROQ_API_KEY;
if (!apiKey) return null;
return {
apiUrl: 'https://api.groq.com/openai/v1/chat/completions',
model: 'llama-3.1-8b-instant',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
};
}
if (provider === 'openrouter') {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) return null;
return {
apiUrl: 'https://openrouter.ai/api/v1/chat/completions',
model: 'openrouter/free',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://worldmonitor.app',
'X-Title': 'World Monitor',
},
};
}
return null;
}
export function stripThinkingTags(text: string): string {
let s = text
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/<\|thinking\|>[\s\S]*?<\|\/thinking\|>/gi, '')
.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, '')
.replace(/<reflection>[\s\S]*?<\/reflection>/gi, '')
.replace(/<\|begin_of_thought\|>[\s\S]*?<\|end_of_thought\|>/gi, '')
.trim();
s = s
.replace(/<think>[\s\S]*/gi, '')
.replace(/<\|thinking\|>[\s\S]*/gi, '')
.replace(/<reasoning>[\s\S]*/gi, '')
.replace(/<reflection>[\s\S]*/gi, '')
.replace(/<\|begin_of_thought\|>[\s\S]*/gi, '')
.trim();
return s;
}
const PROVIDER_CHAIN = ['ollama', 'groq', 'openrouter'] as const;
export interface LlmCallOptions {
messages: Array<{ role: string; content: string }>;
temperature?: number;
maxTokens?: number;
timeoutMs?: number;
provider?: string;
stripThinkingTags?: boolean;
validate?: (content: string) => boolean;
}
export interface LlmCallResult {
content: string;
model: string;
provider: string;
tokens: number;
}
export async function callLlm(opts: LlmCallOptions): Promise<LlmCallResult | null> {
const {
messages,
temperature = 0.3,
maxTokens = 1500,
timeoutMs = 25_000,
provider: forcedProvider,
stripThinkingTags: shouldStrip = true,
validate,
} = opts;
const providers = forcedProvider ? [forcedProvider] : [...PROVIDER_CHAIN];
for (const providerName of providers) {
const creds = getProviderCredentials(providerName);
if (!creds) {
if (forcedProvider) return null;
continue;
}
try {
const resp = await fetch(creds.apiUrl, {
method: 'POST',
headers: { ...creds.headers, 'User-Agent': CHROME_UA },
body: JSON.stringify({
...creds.extraBody,
model: creds.model,
messages,
temperature,
max_tokens: maxTokens,
}),
signal: AbortSignal.timeout(timeoutMs),
});
if (!resp.ok) {
console.warn(`[llm:${providerName}] HTTP ${resp.status}`);
if (forcedProvider) return null;
continue;
}
const data = (await resp.json()) as {
choices?: Array<{ message?: { content?: string } }>;
usage?: { total_tokens?: number };
};
let content = data.choices?.[0]?.message?.content?.trim() || '';
if (!content) {
if (forcedProvider) return null;
continue;
}
const tokens = data.usage?.total_tokens ?? 0;
if (shouldStrip) {
content = stripThinkingTags(content);
if (!content) {
if (forcedProvider) return null;
continue;
}
}
if (validate && !validate(content)) {
console.warn(`[llm:${providerName}] validate() rejected response, trying next`);
if (forcedProvider) return null;
continue;
}
return { content, model: creds.model, provider: providerName, tokens };
} catch (err) {
console.warn(`[llm:${providerName}] ${(err as Error).message}`);
if (forcedProvider) return null;
continue;
}
}
return null;
}