feat(llm): propagate cancellation through reconstruction providers
This commit is contained in:
+42
-20
@@ -290,7 +290,27 @@ export function __resetChatSupportForTests() {
|
||||
_chatSupported = null;
|
||||
}
|
||||
|
||||
async function detectChatSupport(baseUrl, modelName) {
|
||||
function isAbortError(error) {
|
||||
return error?.name === "AbortError";
|
||||
}
|
||||
|
||||
function createRequestAbortController(signal, timeoutMs = 300000) {
|
||||
const controller = new AbortController();
|
||||
const abortFromCaller = () => controller.abort();
|
||||
if (signal?.aborted) controller.abort();
|
||||
else signal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
dispose() {
|
||||
clearTimeout(timeout);
|
||||
signal?.removeEventListener("abort", abortFromCaller);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function detectChatSupport(baseUrl, modelName, signal) {
|
||||
if (_chatSupported !== null) return _chatSupported;
|
||||
|
||||
try {
|
||||
@@ -302,6 +322,7 @@ async function detectChatSupport(baseUrl, modelName) {
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
stream: false,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
@@ -314,7 +335,8 @@ async function detectChatSupport(baseUrl, modelName) {
|
||||
await res.text();
|
||||
_chatSupported = false;
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (isAbortError(error) && signal?.aborted) throw error;
|
||||
_chatSupported = false;
|
||||
}
|
||||
|
||||
@@ -322,7 +344,7 @@ async function detectChatSupport(baseUrl, modelName) {
|
||||
}
|
||||
|
||||
class OllamaLlmProvider {
|
||||
async generateReconstruction(scenario, modelName, outputSchema) {
|
||||
async generateReconstruction(scenario, modelName, outputSchema, { signal } = {}) {
|
||||
// scenario is ALREADY a fully-built prompt text (built by analyseScenario).
|
||||
// Do NOT call buildPrompt() again — that would double-wrap the prompt.
|
||||
const prompt = scenario;
|
||||
@@ -345,18 +367,19 @@ class OllamaLlmProvider {
|
||||
// Step 1: Detect whether /api/chat exists (cache result)
|
||||
// ================================================================
|
||||
try {
|
||||
chatSupported = await detectChatSupport(baseUrl, modelName);
|
||||
} catch { /* failed silently — defaults to false */ }
|
||||
chatSupported = await detectChatSupport(baseUrl, modelName, signal);
|
||||
} catch (error) {
|
||||
if (isAbortError(error) && signal?.aborted) throw error;
|
||||
// Failed silently — defaults to false.
|
||||
}
|
||||
providerExecution.chatCapabilityDetected = chatSupported;
|
||||
|
||||
// ================================================================
|
||||
// Step 2: Try /api/chat if supported with the supplied or reconstruction schema
|
||||
// ================================================================
|
||||
if (chatSupported) {
|
||||
const requestAbort = createRequestAbortController(signal);
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 300000); // 5 min for reconstruction
|
||||
|
||||
apiUsed = "/api/chat";
|
||||
providerExecution.chatRequestAttempted = true;
|
||||
const res = await fetch(`${baseUrl}/api/chat`, {
|
||||
@@ -368,11 +391,9 @@ class OllamaLlmProvider {
|
||||
stream: false,
|
||||
format: outputSchema ?? reconstructionJsonSchema,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
signal: requestAbort.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.ok) {
|
||||
providerExecution.chatRequestSucceeded = true;
|
||||
fullResponseData = await res.json();
|
||||
@@ -384,7 +405,9 @@ class OllamaLlmProvider {
|
||||
await res.text();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.message.includes("abort")) { /* non-fatal */ }
|
||||
if (isAbortError(e)) throw e;
|
||||
} finally {
|
||||
requestAbort.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,10 +416,8 @@ class OllamaLlmProvider {
|
||||
// Use a long timeout — cold starts can take 2-4 minutes for large models.
|
||||
// ================================================================
|
||||
if (rawResponse == null || rawResponse === "") {
|
||||
const requestAbort = createRequestAbortController(signal);
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 300000); // 5 min for cold start
|
||||
|
||||
apiUsed = "/api/generate"; // set BEFORE the request so we know which API failed
|
||||
providerExecution.generateRequestAttempted = true;
|
||||
|
||||
@@ -410,11 +431,9 @@ class OllamaLlmProvider {
|
||||
// No format:json — older Ollama doesn't support it on /api/generate either.
|
||||
// We rely on the strong prompt instruction above instead.
|
||||
}),
|
||||
signal: controller.signal,
|
||||
signal: requestAbort.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`/api/generate returned ${res.status}: ${text.slice(0, 500)}`);
|
||||
@@ -439,7 +458,7 @@ class OllamaLlmProvider {
|
||||
`- Use a smaller model (e.g., llama3.1 instead of llama3.1:70b)\n` +
|
||||
`- Check Ollama logs: \`ollama serve\` or look at your system logs`
|
||||
);
|
||||
if (e?.name === "AbortError" || e instanceof TypeError) {
|
||||
if (isAbortError(e) || e instanceof TypeError) {
|
||||
error.code = "PROVIDER_UNAVAILABLE";
|
||||
}
|
||||
error.providerApiPath = apiUsed;
|
||||
@@ -447,6 +466,8 @@ class OllamaLlmProvider {
|
||||
throw error;
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
requestAbort.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,7 +546,7 @@ class OpenAIReconstructionProvider {
|
||||
this.fetchImpl = fetchImpl;
|
||||
}
|
||||
|
||||
async generateReconstruction(prompt, modelName = "gpt-5.6-terra", outputSchema) {
|
||||
async generateReconstruction(prompt, modelName = "gpt-5.6-terra", outputSchema, { signal } = {}) {
|
||||
if (!this.apiKey) throw new Error("OPENAI_API_KEY is not set");
|
||||
const structuredOutputSchema = outputSchema
|
||||
? createOpenAIStrictSchema(outputSchema, null)
|
||||
@@ -551,6 +572,7 @@ class OpenAIReconstructionProvider {
|
||||
format,
|
||||
},
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user