feat(ai): UI-configurable response timeout, local streaming, dedicated OCR provider

Add three per-user AI settings, all configured in the app (no new env vars),
behind one migration (0198):

- Response timeout (seconds). A per-user upstream timeout threaded onto the
  provider call. A local/self-hosted backend can take more than the legacy
  60 s default to load a model on the first request, which timed the Coach
  out before the first token. Defaults to a generous ~180 s when unset;
  validated to 10–600 s.

- True token streaming for the local OpenAI-compatible client. The client now
  POSTs with stream:true and parses the SSE delta chunks, emitting tokens as
  they arrive, with a per-idle-gap timeout instead of a whole-call ceiling and
  a graceful fallback to the buffered path when a server cannot stream. The
  Coach route runs the provider call inside the response stream behind a
  keepalive heartbeat so a reverse proxy no longer idle-drops the connection
  during model load or generation. Grounding, refusal, and sentinel-stripping
  still run on the complete reply before any token reaches the client.

- Dedicated document-scan (Lab-OCR) provider. Off by default — the OCR path
  uses the main provider chain. When enabled, lab-report processing resolves a
  separate provider/model/key, with a hint when the chosen model is not
  vision-capable. The new key column is AES-256-GCM at rest and registered in
  the encrypted-columns registry and the rotation script.

i18n: EN + DE translations for the new Settings → AI strings (other locales
carry interim copies to keep locale parity green).
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 15:21:40 +02:00
1 parent 3b74b108a1
commit b91649d6de
30 files changed
+2281 -569

No files matched your search

+44
View File
@@ -14370,6 +14370,43 @@ components:
- type: string
- type: "null"
description: "`...` + last 4 chars of the stored OpenAI key, or null."
responseTimeoutSeconds:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: v1.22 (#89) — per-user response timeout for AI generation, in seconds (10–600). Null = the generous
built-in default (~180 s). Mainly for slow local/self-hosted backends.
ocrEnabled:
type: boolean
description: v1.22 (#90) — whether a dedicated document-scan (Lab-OCR) provider is active. When false the OCR path uses
the main provider chain.
ocrProvider:
anyOf:
- type: string
enum:
- OPENAI
- ANTHROPIC
- LOCAL
- type: "null"
description: The dedicated document-scan provider, or null when unset.
ocrModel:
anyOf:
- type: string
- type: "null"
ocrBaseUrl:
anyOf:
- type: string
- type: "null"
description: Custom base URL (LOCAL OCR provider only); null otherwise.
hasOcrKey:
type: boolean
ocrKeyPreview:
anyOf:
- type: string
- type: "null"
description: "`...` + last 4 chars of the stored document-scan provider key, or null."
required:
- provider
- model
@@ -14381,6 +14418,13 @@ components:
- hasLocalKey
- hasOpenaiKey
- openaiKeyPreview
- responseTimeoutSeconds
- ocrEnabled
- ocrProvider
- ocrModel
- ocrBaseUrl
- hasOcrKey
- ocrKeyPreview
additionalProperties: false
description: The calling user's AI-provider configuration plus the effective availability (`aiAvailable` + `managedBy`).
Reports key presence + a masked preview only — never a plaintext key.