From c77c30be83cd0aabc48b3dd546b3996d9ceb91f0 Mon Sep 17 00:00:00 2001 From: altair823 Date: Mon, 4 May 2026 23:26:48 +0900 Subject: [PATCH] =?UTF-8?q?feat(ollama):=20LocalOllamaProvider=20=E2=80=94?= =?UTF-8?q?=20abort()=20+=20AbortController=20instance=20field=20(v0.2.3.1?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - abortController 가 method-local 에서 private instance field 로 이동 - public abort() 메서드 — 외부에서 in-flight generate 강제 중단 - ProviderHolder.replace() 시 호출되어 endpoint 변경 즉시 반영 - 단위 +2 cases (abort cancellation, model 파라미터) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/ai/LocalOllamaProvider.ts | 13 ++++++++++--- tests/unit/LocalOllamaProvider.test.ts | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/ai/LocalOllamaProvider.ts b/src/main/ai/LocalOllamaProvider.ts index d326ad8..44ce522 100644 --- a/src/main/ai/LocalOllamaProvider.ts +++ b/src/main/ai/LocalOllamaProvider.ts @@ -18,6 +18,7 @@ export class LocalOllamaProvider implements InferenceProvider { private timeoutMs: number; private temperature: number; private numPredict: number; + private abortController: AbortController | null = null; constructor(opts: LocalOllamaOptions = {}) { this.endpoint = opts.endpoint ?? 'http://localhost:11434'; @@ -29,8 +30,8 @@ export class LocalOllamaProvider implements InferenceProvider { } async generate(input: GenerateInput): Promise { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), this.timeoutMs); + this.abortController = new AbortController(); + const timer = setTimeout(() => this.abortController?.abort(), this.timeoutMs); try { const res = await request(`${this.endpoint}/api/generate`, { method: 'POST', @@ -42,7 +43,7 @@ export class LocalOllamaProvider implements InferenceProvider { stream: false, options: { temperature: this.temperature, num_predict: this.numPredict } }), - signal: controller.signal + signal: this.abortController.signal }); if (res.statusCode < 200 || res.statusCode >= 300) { throw new Error(`ollama http ${res.statusCode}`); @@ -55,9 +56,15 @@ export class LocalOllamaProvider implements InferenceProvider { return parseAiResponse(parsed); } finally { clearTimeout(timer); + this.abortController = null; } } + /** v0.2.3.1 — 외부에서 in-flight generate 강제 중단. ProviderHolder.replace 시 사용. */ + abort(): void { + this.abortController?.abort(); + } + async healthCheck(): Promise { try { const res = await request(`${this.endpoint}/api/tags`, { method: 'GET' }); diff --git a/tests/unit/LocalOllamaProvider.test.ts b/tests/unit/LocalOllamaProvider.test.ts index a9d60fb..eea7c5c 100644 --- a/tests/unit/LocalOllamaProvider.test.ts +++ b/tests/unit/LocalOllamaProvider.test.ts @@ -89,4 +89,24 @@ describe('LocalOllamaProvider', () => { expect(h.ok).toBe(false); expect(h.reason).toMatch(/connect|refused|unreachable/i); }); + + it('abort() cancels in-flight generate (rejects with AbortError)', async () => { + mock.get('http://localhost:11434').intercept({ + path: '/api/generate', method: 'POST' + }).reply((async () => { + await new Promise((r) => setTimeout(r, 5000)); // long-running + return { statusCode: 200, data: '{}' }; + }) as never); + const provider = new LocalOllamaProvider({ timeoutMs: 30_000 }); + const generatePromise = provider.generate({ + text: 'x', todayKst: '2026-05-04', dueDateCandidates: [] + }); + setTimeout(() => provider.abort(), 50); + await expect(generatePromise).rejects.toThrow(); + }); + + it('constructor uses provided model param (not just default)', () => { + const provider = new LocalOllamaProvider({ model: 'gemma4:26b' }); + expect(provider.name).toBe('local-ollama/gemma4:26b'); + }); });