perf: switch all AI calls to native Ollama endpoint, add streaming + keep_alive
Tüm AI çağrıları artık ortak lib/aiClient.ts üzerinden gidiyor: - /v1/chat/completions yerine native /api/chat (think:false burada güvenilir çalışıyor) - keep_alive:30m ile model her istekte bellekten atılıp soğuk başlamıyor - num_predict ile öngörülemez uzunlukta üretime üst sınır - İnteraktif sohbet uç noktaları (chatMessage, sendThreadMessage) artık SSE ile token token akıtıyor - Dosya sohbetinde varsayılan olarak belgenin tam metni değil analiz özeti gönderiliyor; tam metin sadece kullanıcı açıkça isterse eklenir Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0318f2ac77
commit
bcc78ce304
@@ -0,0 +1,124 @@
|
||||
import { getAiConfig } from './aiConfig';
|
||||
|
||||
export interface ChatMsg { role: string; content: string; }
|
||||
|
||||
// Model her istekte bellekten atılıp yeniden yüklenmesin diye (varsayılan Ollama
|
||||
// davranışı: 5 dakika kullanılmazsa boşalır, sıradaki istek soğuk başlangıç
|
||||
// gecikmesi yaşar) worker'ı bu süre boyunca bellekte tutmasını istiyoruz.
|
||||
const KEEP_ALIVE = '30m';
|
||||
const DEFAULT_NUM_PREDICT = 800;
|
||||
const DEFAULT_TIMEOUT_MS = 90_000;
|
||||
const DEFAULT_STALL_TIMEOUT_MS = 60_000;
|
||||
|
||||
function buildBody(messages: ChatMsg[], model: string, stream: boolean, numPredict: number) {
|
||||
return {
|
||||
model,
|
||||
messages,
|
||||
think: false,
|
||||
stream,
|
||||
keep_alive: KEEP_ALIVE,
|
||||
// num_predict: modelin üretebileceği maksimum token sayısı — öngörülemez
|
||||
// uzunlukta "düşünme"/üretim sürelerine karşı üst sınır.
|
||||
options: { num_predict: numPredict },
|
||||
};
|
||||
}
|
||||
|
||||
// Tek seferlik (streaming olmayan) çağrı — başlık üretimi, belge analizi, dilekçe
|
||||
// taslağı, içtihat sorgu optimizasyonu gibi kullanıcının token token izlemediği,
|
||||
// arka planda tamamlanması yeterli olan işler için. Ollama'nın native /api/chat
|
||||
// uç noktasını kullanır — think:false burada güvenilir çalışıyor (OpenAI-uyumluluk
|
||||
// katmanı /v1/chat/completions'ta güvenilir değil).
|
||||
export async function callOllama(
|
||||
messages: ChatMsg[],
|
||||
opts: { numPredict?: number; timeoutMs?: number } = {}
|
||||
): Promise<string> {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const ctrl = new AbortController();
|
||||
const timeoutId = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(buildBody(messages, aiModel, false, opts.numPredict ?? DEFAULT_NUM_PREDICT)),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return (data.message?.content || '').toString().trim();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
// Akışlı (streaming) çağrı — kullanıcının yanıtı token token canlı gördüğü
|
||||
// etkileşimli sohbet uç noktaları için. Ollama /api/chat stream:true ile
|
||||
// newline-delimited JSON döner (her satır { message: { content }, done } içerir).
|
||||
// onToken her parça geldiğinde çağrılır; dönüş değeri tüm yanıtın birleştirilmiş hali.
|
||||
// Sabit bir toplam süre sınırı YOK (kullanıcı üretilmekte olan metni zaten görüyor) —
|
||||
// bunun yerine art arda gelen parçalar arasında stallTimeoutMs kadar sessizlik olursa
|
||||
// (model takıldı / bağlantı koptu) isteği iptal ediyoruz.
|
||||
export async function streamOllama(
|
||||
messages: ChatMsg[],
|
||||
onToken: (delta: string) => void,
|
||||
opts: { numPredict?: number; stallTimeoutMs?: number } = {}
|
||||
): Promise<string> {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const stallMs = opts.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
||||
const ctrl = new AbortController();
|
||||
let stallTimer: ReturnType<typeof setTimeout>;
|
||||
const resetStall = () => {
|
||||
clearTimeout(stallTimer);
|
||||
stallTimer = setTimeout(() => ctrl.abort(), stallMs);
|
||||
};
|
||||
resetStall();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(buildBody(messages, aiModel, true, opts.numPredict ?? DEFAULT_NUM_PREDICT)),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
let full = '';
|
||||
let buffer = '';
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const consumeLine = (line: string) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
let json: any;
|
||||
try {
|
||||
json = JSON.parse(trimmed);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const delta = json.message?.content || '';
|
||||
if (delta) {
|
||||
full += delta;
|
||||
onToken(delta);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
resetStall();
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
lines.forEach(consumeLine);
|
||||
}
|
||||
if (buffer.trim()) consumeLine(buffer);
|
||||
|
||||
return full.trim();
|
||||
} finally {
|
||||
clearTimeout(stallTimer!);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user