llm timeout 수정
This commit is contained in:
@@ -23,6 +23,21 @@
|
|||||||
chunked_transfer_encoding on;
|
chunked_transfer_encoding on;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── LLM / AI 검증 (Ta4j 재평가 + LLM 최대 120s — 일반 /api/ 60s 초과) ───
|
||||||
|
location ~ ^/api/strategy/evaluation/(ai-verify|llm-test)$ {
|
||||||
|
proxy_pass http://backend:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 30s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_buffering off;
|
||||||
|
client_max_body_size 16m;
|
||||||
|
}
|
||||||
|
|
||||||
# ── GoldenChart Spring Boot Backend 프록시 ────────────────────────
|
# ── GoldenChart Spring Boot Backend 프록시 ────────────────────────
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend:8080;
|
proxy_pass http://backend:8080;
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ const StrategyEvaluationAiVerifyModal: React.FC<Props> = ({
|
|||||||
<div className="seval-ai-verify-loading" aria-live="polite">
|
<div className="seval-ai-verify-loading" aria-live="polite">
|
||||||
<span className="btd-run-spinner seval-ai-verify-spinner" aria-hidden />
|
<span className="btd-run-spinner seval-ai-verify-spinner" aria-hidden />
|
||||||
<p>LLM이 평가 결과와 차트 시그널을 분석 중입니다…</p>
|
<p>LLM이 평가 결과와 차트 시그널을 분석 중입니다…</p>
|
||||||
<p className="seval-ai-verify-loading-hint">최대 2분 정도 걸릴 수 있습니다.</p>
|
<p className="seval-ai-verify-loading-hint">Ta4j 재평가와 LLM 분석에 최대 2~3분 걸릴 수 있습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -118,17 +118,69 @@ async function requestOrThrow<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
const text = await res.text().catch(() => '');
|
const text = await res.text().catch(() => '');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
let msg = `요청 실패 (${res.status})`;
|
let msg = `요청 실패 (${res.status})`;
|
||||||
try {
|
if (res.status === 504) {
|
||||||
const j = JSON.parse(text) as { message?: string };
|
msg = '게이트웨이 시간 초과(504). LLM 분석은 2~3분 걸릴 수 있습니다. 잠시 후 다시 시도해 주세요.';
|
||||||
if (j.message) msg = j.message;
|
} else {
|
||||||
} catch { /* plain text */ }
|
try {
|
||||||
if (!text.startsWith('{') && text) msg = text.slice(0, 200);
|
const j = JSON.parse(text) as { message?: string };
|
||||||
|
if (j.message) msg = j.message;
|
||||||
|
} catch { /* plain text */ }
|
||||||
|
if (!text.startsWith('{') && text) {
|
||||||
|
const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
if (plain) msg = plain.slice(0, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
if (res.status === 204 || !text) return undefined as T;
|
if (res.status === 204 || !text) return undefined as T;
|
||||||
return JSON.parse(text) as T;
|
return JSON.parse(text) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** LLM·AI 검증 등 장시간 API */
|
||||||
|
async function requestOrThrowWithTimeout<T>(
|
||||||
|
path: string,
|
||||||
|
init?: RequestInit,
|
||||||
|
timeoutMs = 200_000,
|
||||||
|
): Promise<T> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
|
...init,
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { ...authHeaders(), ...(init?.headers ?? {}) },
|
||||||
|
});
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
if (!res.ok) {
|
||||||
|
let msg = `요청 실패 (${res.status})`;
|
||||||
|
if (res.status === 504) {
|
||||||
|
msg = '게이트웨이 시간 초과(504). LLM 분석은 2~3분 걸릴 수 있습니다. 잠시 후 다시 시도해 주세요.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(text) as { message?: string };
|
||||||
|
if (j.message) msg = j.message;
|
||||||
|
} catch { /* plain text */ }
|
||||||
|
if (!text.startsWith('{') && text) {
|
||||||
|
const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
if (plain) msg = plain.slice(0, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
if (res.status === 204 || !text) return undefined as T;
|
||||||
|
return JSON.parse(text) as T;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof DOMException && e.name === 'AbortError') {
|
||||||
|
throw new Error(
|
||||||
|
`요청 시간 초과(${Math.round(timeoutMs / 1000)}초). LLM이 느리면 설정 > LLM에서 타임아웃을 늘리거나 잠시 후 다시 시도하세요.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T | null> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}${path}`, {
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
@@ -1690,11 +1742,15 @@ export interface StrategyEvaluationAiVerifyResponse {
|
|||||||
export async function fetchStrategyEvaluationAiVerify(
|
export async function fetchStrategyEvaluationAiVerify(
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
): Promise<StrategyEvaluationAiVerifyResponse> {
|
): Promise<StrategyEvaluationAiVerifyResponse> {
|
||||||
return requestOrThrow<StrategyEvaluationAiVerifyResponse>('/strategy/evaluation/ai-verify', {
|
return requestOrThrowWithTimeout<StrategyEvaluationAiVerifyResponse>(
|
||||||
method: 'POST',
|
'/strategy/evaluation/ai-verify',
|
||||||
cache: 'no-store',
|
{
|
||||||
body: JSON.stringify({ context }),
|
method: 'POST',
|
||||||
});
|
cache: 'no-store',
|
||||||
|
body: JSON.stringify({ context }),
|
||||||
|
},
|
||||||
|
200_000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LlmConnectionTestResponse {
|
export interface LlmConnectionTestResponse {
|
||||||
@@ -1706,10 +1762,14 @@ export interface LlmConnectionTestResponse {
|
|||||||
|
|
||||||
/** 설정 화면 — LLM 서버 연결 테스트 (저장된 설정 기준) */
|
/** 설정 화면 — LLM 서버 연결 테스트 (저장된 설정 기준) */
|
||||||
export async function fetchLlmConnectionTest(): Promise<LlmConnectionTestResponse> {
|
export async function fetchLlmConnectionTest(): Promise<LlmConnectionTestResponse> {
|
||||||
return requestOrThrow<LlmConnectionTestResponse>('/strategy/evaluation/llm-test', {
|
return requestOrThrowWithTimeout<LlmConnectionTestResponse>(
|
||||||
method: 'POST',
|
'/strategy/evaluation/llm-test',
|
||||||
cache: 'no-store',
|
{
|
||||||
});
|
method: 'POST',
|
||||||
|
cache: 'no-store',
|
||||||
|
},
|
||||||
|
130_000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 전략 DSL에 포함된 평가 시간봉 목록 (실시간 체크·STOMP 구독용) */
|
/** 전략 DSL에 포함된 평가 시간봉 목록 (실시간 체크·STOMP 구독용) */
|
||||||
|
|||||||
Reference in New Issue
Block a user