feat(ai): send a per-entry reasoning effort to Local and gateway providers

Reasoning models on Ollama and similar servers think by default and can
spend the whole max_tokens budget before the visible answer starts, so
the call comes back empty or times out. The Local and OpenAI-compatible
chain entries now carry an optional reasoning setting: Default sends
nothing, Off sends reasoning_effort "none", Low / Medium / High send
their own value. It is stored on the entry in aiProviderChain, so a
chain can mix a thinking model with one that does not know the field.

- Every exported resolver binds the entry's value onto the client, the
  connection test included, keyed on the instance type so the operator
  provider, OpenAI, Anthropic and Codex never carry it.
- PATCH /api/user/ai-provider accepts localReasoningEffort and
  compatReasoningEffort and GET reports them. A provider absent from
  the chain holds its value on a disabled entry; restating Default on a
  chain that was never customised writes nothing.
- PUT /api/insights/provider-chain accepts reasoningEffort on local and
  openai-compatible entries, refuses it on any other, and keeps the
  stored value when the body leaves it out. Its schema is now shared
  with the OpenAPI registry.
- A Default request body is byte-identical to the one sent before.

Refs #1126
This commit is contained in:
Marc-André Bombeck committed 2026-10-04 10:42:59 +02:00
1 parent 808b2672cd
commit befac59576
18 files changed
+1350 -84

No files matched your search

+6 -31
View File
@@ -18,7 +18,7 @@ import {
SPARKLINE_MAX_POINTS,
} from "@/lib/insights/derived/types";
import { ANALYTICS_RANGES } from "@/lib/analytics/range-delta";
import { PROVIDER_CHAIN_TYPES } from "@/lib/ai/provider-chain";
import { providerChainPutSchema } from "@/lib/validations/ai-provider";
import { PERIOD_DAYS } from "@/lib/insights/narrative/period-narrative";
import { aiCapabilityState } from "../profile";
@@ -2531,36 +2531,11 @@ export const providerChainResponse = z
"The account's AI-provider chain as the settings surface reads it. Provider TYPES and boolean state only — no key, token or account id is ever on this wire.",
});
export const providerChainPutRequest = z
.object({
chain: z
.array(
z.object({
providerType: z
.enum(PROVIDER_CHAIN_TYPES as unknown as [string, ...string[]])
.describe(
"Closed allow-list. The mock provider is excluded from it structurally, which is what keeps production from reaching one.",
),
priority: z
.number()
.int()
.optional()
.describe(
"ACCEPTED AND IGNORED. Priority is recomputed from the array's insertion order, so a stale client cannot persist a chain whose displayed order disagrees with its stored one. Send the order you want as the order of the array.",
),
enabled: z.boolean(),
}),
)
.min(1)
.describe(
"The whole chain, in the order it should be walked. At least one entry and at most one per known provider type; a repeated type is refused.",
),
})
.meta({
id: "ProviderChainPutRequest",
description:
"Replaces the account's provider chain wholesale. No credential is written or read here — only which provider types exist, in what order, and which are enabled.",
});
export const providerChainPutRequest = providerChainPutSchema.meta({
id: "ProviderChainPutRequest",
description:
"Replaces the account's provider chain wholesale. No credential is written or read here — only which provider types exist, in what order, which are enabled, and the reasoning setting of the Local and OpenAI-compatible entries.",
});
// ── Read-only advisor + generation (`/api/insights/generate`) ─────────
+7
View File
@@ -36,6 +36,7 @@ import {
} from "@/lib/validations/user-prefs";
import {
aiProviderPatchSchema,
reasoningEffortSchema,
aiTestOverrideSchema,
} from "@/lib/validations/ai-provider";
import {
@@ -674,6 +675,12 @@ const aiProviderResponse = z
.describe(
"v1.22 (#89) — per-user response timeout for AI generation, in seconds (10–600). Null = the built-in comprehensive-briefing default (~120 s). Mainly for slow local/self-hosted backends.",
),
localReasoningEffort: reasoningEffortSchema.describe(
"#1126 — reasoning setting of the Local provider entry, sent as `reasoning_effort` (`none` = Off). Null = Default: the key is not sent.",
),
compatReasoningEffort: reasoningEffortSchema.describe(
"#1126 — the same for the OpenAI-compatible gateway entry.",
),
// v1.38.19 — the setup flow's last screen used to promise
// that insights "work for you right away" as soon as `managedBy` read
// `server`. That is a presence read; it never knew whether the key