LearnNewsExamplesServices
Frontmatter
id12814
titleAlign the Ollama + OpenAiCompatible interactive-timeout contracts
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-grace
createdAtJun 9, 2026, 10:33 AM
updatedAtJun 9, 2026, 12:52 PM
githubUrlhttps://github.com/neomjs/neo/issues/12814
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 9, 2026, 12:52 PM

Align the Ollama + OpenAiCompatible interactive-timeout contracts

Closed v13.0.0/archive-v13-0-0-chunk-17 enhancementaiarchitecture
neo-opus-vega
neo-opus-vega commented on Jun 9, 2026, 10:33 AM

Context

Follow-up to PR #12806 (@tobiu-requested). #12806 gave Ollama.generate an interactive timeoutMs (it had none — it hardcoded a 1h socket timeout). OpenAiCompatible.generate already had its own timeout handling. The two chat providers are parallel implementations, and #12806 — while fixing the Ollama gap — left their timeout contracts diverged. They need to stay in sync.

Live latest-open sweep: checked the latest 20 open issues at 2026-06-09T08:35Z; no equivalent open ticket (the matches — #12803, #12487, #12509, #12742 — are all closed).

The Problem

A caller (SearchService.ask today; a daemon-yield path later) gets different timeout behavior depending on which provider is active. Two confirmed divergences (V-B-A'd at the source):

  1. Error shape — a caller cannot uniformly detect a provider timeout. OpenAiCompatible throws a structured error with error.code = 'OPENAI_COMPATIBLE_TIMEOUT' (ai/provider/OpenAiCompatible.mjs:16-18). Ollama (post-#12806) throws a plain Error ([Ollama] … timed out after Nms (host=, model=)) with no .code. A caller checking err.code sees one provider's timeout and not the other's.
  2. options.signal (upstream cancellation). OpenAiCompatible honors it — full AbortSignal wiring (OpenAiCompatible.mjs:257-277: adopts an upstream signal into the AbortController). Ollama strips options.signal (Ollama.mjs payload-prep) and ignores upstream cancellation. (This is the signal-parity item deliberately deferred out of #12806 — see the #12803 AC-correction, IC_kwDODSospM8AAAABFZtiXg.)

The message format is already roughly parallel ([Provider] {label} timed out after {ms}ms (host=, model=)), so the gap is the detectable code + signal support — and, structurally, the absence of a shared contract that would keep the two from drifting further as either evolves.

The Architectural Reality

  • ai/provider/OpenAiCompatible.mjscreateTimeoutError({operationLabel, timeoutMs, host, modelName})error.code = 'OPENAI_COMPATIBLE_TIMEOUT' (:16-18); upstream-signal adoption + timeout AbortController (:257-280); transport is fetch (controller.signalfetch).
  • ai/provider/Ollama.mjs (post-#12806) — socket timeout via req.on('timeout')req.destroy() + a plain Error (no .code); options.signal stripped in payload-prep; transport is node http.request.
  • Both consumed via ai/provider/buildChatModel.mjsSearchService.ask; a future signal-consumer is the LEAF-2 daemon-yield (#12799).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Ollama.generate(options.signal) this ticket honor the upstream AbortSignal (node http.request accepts signal natively → destroy on abort) current: stripped/ignored ai/provider/Ollama.mjs OpenAiCompatible.mjs:257-277 is the reference impl
{Ollama,OpenAiCompatible}.generate timeout error this ticket uniform detectable shape — a shared .code (or shared error type) + the structured fields current: per-provider (.code only on OpenAiCompatible) ai/provider/*.mjs OpenAiCompatible.mjs:16-18 ('OPENAI_COMPATIBLE_TIMEOUT') is the precedent to generalize

Decision Record impact

none (provider-internal contract alignment; no ADR governs the provider abstraction surface today).

The Fix

Align both providers on one interactive-timeout contract:

  1. Signal parityOllama.generate honors options.signal (wire it to http.request's native signal; destroy + throw the timeout-shaped error on abort).
  2. Detectable uniform error — a shared timeout-error helper (one .code, e.g. PROVIDER_TIMEOUT, or a shared error type) so callers detect a provider timeout regardless of which provider served the call. Generalize createTimeoutError rather than duplicate it.
  3. Sync by construction — house the shared error helper + a documented generate(options) contract (timeoutMs, signal, operationLabel) in one place so the providers can't silently drift again. The transport mechanism may differ (fetch+AbortController vs http.request); only the observable contract must be uniform.

Acceptance Criteria

  • Ollama.generate honors options.signal (upstream abort → request destroyed → timeout-shaped error), matching OpenAiCompatible.
  • Both providers throw a timeout error with a uniform, caller-detectable shape (shared .code or shared type), so SearchService (and future consumers) can branch on a provider timeout without per-provider knowledge.
  • The shared timeout-error shape lives in one helper (generalized from createTimeoutError); the generate(options) timeout/signal contract is documented at the shared site.
  • Provider-level regressions for both providers (the new signal abort + the uniform error shape) — extend KeepAlive.spec.mjs (hung loopback for Ollama; fetch-mock for OpenAiCompatible).

Out of Scope

  • The ask_knowledge_base synthesis-timeout wiring (#12806 — done).
  • The daemon-yield itself (#12799 LEAF 2 — a future consumer of signal, not this contract work).
  • Other provider-parity dimensions (keep_alive, streaming) — already aligned or separately tracked.

Avoided Traps

  • Not forcing identical mechanisms — fetch+AbortController vs http.request socket timeout may differ; only the observable contract (error shape + signal honoring) must be uniform.
  • Not speculative over-abstraction — a shared helper is justified by a concrete, demonstrated divergence (#12806) and an operator "stay in sync" directive, not by gold-plating a single-provider path.

Related

#12806 (introduced the divergence), #12803 (origin ticket + signal-deferral AC-correction IC_kwDODSospM8AAAABFZtiXg), #12799 (LEAF 2 — likely first signal consumer), #10494, #12740 (local-first provider defaults epic).

Origin Session ID: 728442d6-a2a0-4481-a631-a3112ce6d703

Retrieval Hint: query_raw_memories("Ollama OpenAiCompatible provider interactive timeout contract parity signal error code")

Authored by @neo-opus-vega (Claude Opus 4.8).

tobiu referenced in commit 3bffd3e - "fix(ai): align Ollama + OpenAiCompatible interactive-timeout contracts (#12814) (#12818) on Jun 9, 2026, 12:52 PM
tobiu closed this issue on Jun 9, 2026, 12:52 PM