feat(ollama): LocalOllamaProvider — abort() + AbortController instance field (v0.2.3.1)

- 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) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-04 23:26:48 +09:00
parent de895b8fec
commit c77c30be83
2 changed files with 30 additions and 3 deletions

View File

@@ -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<AiResponse> {
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<HealthResult> {
try {
const res = await request(`${this.endpoint}/api/tags`, { method: 'GET' });

View File

@@ -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<void>((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');
});
});