feat: refactor AI engine logic, remove cross-engine fallback, add retry with backoff, and bump version to 2.80.18
This commit is contained in:
+47
-27
@@ -1942,35 +1942,37 @@ export class AgentExecutor {
|
||||
temperature: number;
|
||||
}): Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }> {
|
||||
const { baseUrl, modelName, reqMessages, temperature } = params;
|
||||
const primaryEngine = resolveEngine(baseUrl);
|
||||
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||
const engine = resolveEngine(baseUrl); // 사용자가 설정한 엔진만 사용
|
||||
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
|
||||
const messageVariants = this.buildEngineMessageVariants(reqMessages, engine);
|
||||
const modelCandidates = this.buildModelCandidates(modelName, engine);
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const engine of engines) {
|
||||
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
|
||||
const messageVariants = this.buildEngineMessageVariants(reqMessages, engine);
|
||||
const modelCandidates = this.buildModelCandidates(modelName, engine);
|
||||
|
||||
for (const candidateModel of modelCandidates) {
|
||||
for (const variant of messageVariants) {
|
||||
const streamBody = {
|
||||
model: candidateModel,
|
||||
messages: variant.messages,
|
||||
stream: true,
|
||||
...(engine === 'lmstudio'
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||
};
|
||||
// 같은 엔진 내에서만 model candidate / message variant retry
|
||||
for (const candidateModel of modelCandidates) {
|
||||
for (const variant of messageVariants) {
|
||||
const streamBody = {
|
||||
model: candidateModel,
|
||||
messages: variant.messages,
|
||||
stream: true,
|
||||
...(engine === 'lmstudio'
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||
};
|
||||
|
||||
// 일시적 네트워크 오류용 retry (최대 2회, 지수 backoff)
|
||||
const MAX_RETRIES = 2;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
const delay = 500 * Math.pow(2, attempt - 1); // 500ms, 1000ms
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
logInfo('AI streaming request retry.', { engine, attempt, model: candidateModel });
|
||||
}
|
||||
logInfo('AI streaming request started.', {
|
||||
engine,
|
||||
apiUrl,
|
||||
model: candidateModel,
|
||||
variant: variant.name,
|
||||
messageCount: variant.messages.length,
|
||||
roles: variant.messages.map(message => message.role),
|
||||
firstUserPreview: summarizeText(String(variant.messages.find(message => message.role === 'user')?.content || ''), 300)
|
||||
engine, apiUrl, model: candidateModel,
|
||||
variant: variant.name, messageCount: variant.messages.length,
|
||||
attempt
|
||||
});
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
@@ -1988,7 +1990,12 @@ export class AgentExecutor {
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
lastError = new Error(`AI Engine error (${engine}/${variant.name}): ${response.status} - ${summarizeText(errText, 300)}`);
|
||||
logError('AI streaming request returned non-OK status.', { engine, variant: variant.name, apiUrl, status: response.status, body: summarizeText(errText, 500) });
|
||||
logError('AI streaming request returned non-OK status.', {
|
||||
engine, variant: variant.name, apiUrl,
|
||||
status: response.status, body: summarizeText(errText, 500)
|
||||
});
|
||||
// 4xx는 재시도해도 의미없음. 5xx만 재시도.
|
||||
if (response.status >= 400 && response.status < 500) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1996,13 +2003,26 @@ export class AgentExecutor {
|
||||
return { response, engine, apiUrl };
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logError('AI streaming request failed.', { engine, variant: variant.name, apiUrl, model: candidateModel, error: lastError.message });
|
||||
// AbortError는 사용자가 취소한 것이므로 retry 금지
|
||||
if (lastError.name === 'AbortError') {
|
||||
throw lastError;
|
||||
}
|
||||
logError('AI streaming request failed.', {
|
||||
engine, variant: variant.name, apiUrl, model: candidateModel,
|
||||
attempt, error: lastError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Unable to connect to AI engine.');
|
||||
// 명확한 에러 메시지: 어느 엔진이 실패했는지 사용자에게 알림
|
||||
const engineLabel = engine === 'lmstudio' ? 'LM Studio' : 'Ollama';
|
||||
throw new Error(
|
||||
`${engineLabel} 엔진에 연결할 수 없습니다. ` +
|
||||
`${engineLabel}가 실행 중이고 모델 '${modelName}'이 로드되어 있는지 확인하세요. ` +
|
||||
`(원인: ${lastError?.message || 'unknown'})`
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeMessages(messages: ChatMessage[]) {
|
||||
|
||||
Reference in New Issue
Block a user