feat(ai): resolve the reasoning level against the operator's controls

Add resolveReasoning, the one decision for how hard a call may think:
the operator's switch, then the provider's ability, then the requested
level (the person's Coach preference, or a background job's own level),
then the operator's cap, then a medium ceiling when the operator pays.
The answer names the layer that decided it.

loadReasoningControls reads the two app_settings columns, memoised per
request, read afresh in background runs, and fails closed to "off".
loadCoachReasoningState resolves the published Coach state from the
memoised provider presence; resolveJobReasoning resolves one background
call and never fails it.

A guard proves the switch and the cap over the whole input space and
requires every call that carries reasoning to take it from the
resolver.
This commit is contained in:
Marc-André Bombeck committed 2026-10-06 19:44:51 +02:00
1 parent 6112cfc6d5
commit 74e68745c1
5 files changed
+906 -1

No files matched your search

@@ -0,0 +1,151 @@
/**
* v1.41 — the operator's reasoning switch and cap cannot be bypassed.
*
* Two halves.
*
* Behaviour: `resolveReasoning` is the one decision. With the switch off it
* answers `off` for the Coach and every background job, whatever the person
* chose, whoever pays and whatever the provider can do; with it on, no answer
* is above the cap. Proven exhaustively over the whole input space.
*
* Structure: a call carries reasoning only through `CompletionParams.reasoning`.
* Every production file outside the provider clients that puts that key on a
* call must take its value from the resolver (`@/lib/ai/reasoning/resolve` or
* `@/lib/ai/reasoning/controls`). A file that hand-built `{ effort: "high" }`
* would skip the switch, and goes red here.
*
* Its limit: the structural half reads source text for the setter shapes in
* use (`reasoning: { effort`, `{ reasoning }`, `reasoning: completionReasoning(`,
* `reasoning: resolveJobReasoning(`). A value laundered through another name
* slips it; the resolver import is still required of the file that sets it.
*
* Mutation check: replace `resolveJobReasoning(...)` in `status-provider.ts`
* with `{ effort: "high", summaries: false }` and drop the import: red.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
BACKGROUND_REASONING_JOBS,
REASONING_LEVELS,
REASONING_MAX_EFFORTS,
} from "@/lib/ai/reasoning/levels";
import {
completionReasoning,
resolveReasoning,
type ReasoningSurface,
} from "@/lib/ai/reasoning/resolve";
import { walkSourceFiles } from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
const SURFACES: ReasoningSurface[] = ["coach", ...BACKGROUND_REASONING_JOBS];
describe("the switch and the cap win, behaviourally", () => {
const supports = [
null,
{
effort: true,
summaries: true,
liveSummaries: true,
stateRoundTrip: true,
offIsReal: false,
},
];
it("off reaches no surface as anything but off", () => {
let cases = 0;
for (const surface of SURFACES)
for (const userPref of REASONING_LEVELS)
for (const costOwner of ["user", "operator"] as const)
for (const support of supports) {
const r = resolveReasoning({
surface,
userPref,
admin: { enabled: false, maxEffort: "high" },
costOwner,
support,
});
expect(r.effort).toBe("off");
expect(r.source).toBe("admin_off");
const sent = completionReasoning(r, surface);
// A job sends nothing; the Coach sends an explicit off.
if (surface === "coach") {
expect(sent).toEqual({ effort: "off", summaries: false });
} else {
expect(sent).toBeUndefined();
}
cases++;
}
expect(cases).toBeGreaterThanOrEqual(100);
});
it("no surface ever resolves above the cap", () => {
for (const surface of SURFACES)
for (const userPref of REASONING_LEVELS)
for (const maxEffort of REASONING_MAX_EFFORTS) {
const r = resolveReasoning({
surface,
userPref,
admin: { enabled: true, maxEffort },
costOwner: "user",
});
expect(REASONING_LEVELS.indexOf(r.effort)).toBeLessThanOrEqual(
REASONING_LEVELS.indexOf(maxEffort),
);
}
});
});
/** The provider clients map an already-resolved value onto their wire. */
const WIRE_LAYER = (rel: string) =>
/^lib\/ai\/[a-z-]+-client\.ts$/.test(rel) ||
rel === "lib/ai/provider-runner.ts" ||
rel.startsWith("lib/ai/reasoning/");
const SETTERS = [
/\breasoning\s*:\s*\{\s*effort\b/,
/\{\s*reasoning\s*\}/,
/\breasoning\s*:\s*(?:completionReasoning|resolveJobReasoning)\s*\(/,
];
const RESOLVER_IMPORT =
/from\s+["']@\/lib\/ai\/reasoning\/(?:resolve|controls)["']/;
function stripComments(source: string): string {
return source
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(^|[^:])\/\/.*$/gm, "$1");
}
describe("every call that carries reasoning took it from the resolver", () => {
const files = walkSourceFiles(SRC, { floor: 3000 })
.filter((p) => !p.startsWith("generated/"))
.filter((p) => !p.includes("__tests__"))
.filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx"))
// Client components bind a form field called `reasoning`, not a call.
.filter((p) => !p.startsWith("components/"))
.filter((p) => !WIRE_LAYER(p));
const setters = files.filter((rel) => {
const code = stripComments(readFileSync(join(SRC, rel), "utf8"));
return SETTERS.some((re) => re.test(code));
});
it("finds the setters (an empty set is a failure, not a pass)", () => {
expect(setters.length).toBeGreaterThanOrEqual(2);
expect(setters).toContain("lib/insights/status-provider.ts");
expect(setters).toContain("lib/insights/briefing-provider.ts");
});
it("each one imports the resolver", () => {
expect(
setters.filter(
(rel) => !RESOLVER_IMPORT.test(readFileSync(join(SRC, rel), "utf8")),
),
"A call carries reasoning that did not come from resolveReasoning, so the operator's switch and cap do not reach it.",
).toEqual([]);
});
});
+1 -1
View File
@@ -183,7 +183,7 @@ function buildUserProvider(row: UserAIRow): AIProvider | null {
* An unparseable value answers false and keeps the OpenAI path, which is the
* behaviour that value had before this function existed.
*/
function isAnthropicBaseUrl(baseUrl: string): boolean {
export function isAnthropicBaseUrl(baseUrl: string): boolean {
let host: string;
try {
host = new URL(baseUrl).hostname.toLowerCase();
@@ -0,0 +1,318 @@
import { describe, expect, it } from "vitest";
import {
BACKGROUND_REASONING_JOBS,
REASONING_LEVELS,
REASONING_MAX_EFFORTS,
} from "../levels";
import {
coachReasoningCeiling,
completionReasoning,
parseReasoningMaxEffort,
reasoningSupportForChainEntry,
resolveReasoning,
type ReasoningSurface,
} from "../resolve";
import type { ReasoningSupport } from "../support";
const ON = { enabled: true, maxEffort: "high" } as const;
const FULL: ReasoningSupport = {
effort: true,
summaries: true,
liveSummaries: true,
stateRoundTrip: true,
offIsReal: true,
};
const NO_REASONING: ReasoningSupport = { ...FULL, effort: false };
const SURFACES: ReasoningSurface[] = ["coach", ...BACKGROUND_REASONING_JOBS];
const rank = (l: string) => (REASONING_LEVELS as readonly string[]).indexOf(l);
describe("resolveReasoning — the Coach", () => {
it("runs the person's level when nothing caps it", () => {
for (const pref of REASONING_LEVELS) {
expect(
resolveReasoning({
surface: "coach",
userPref: pref,
admin: ON,
costOwner: "user",
support: FULL,
}),
).toEqual({ effort: pref, summaries: pref !== "off", source: "user" });
}
});
it("defaults a person who never chose to medium", () => {
const r = resolveReasoning({
surface: "coach",
admin: ON,
costOwner: "user",
});
expect(r).toEqual({ effort: "medium", summaries: true, source: "user" });
});
it("clamps the person's level to the operator's cap", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "high",
admin: { enabled: true, maxEffort: "low" },
costOwner: "user",
support: FULL,
});
expect(r).toEqual({ effort: "low", summaries: true, source: "admin_cap" });
});
it("holds an operator-funded turn to medium", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "high",
admin: ON,
costOwner: "operator",
support: FULL,
});
expect(r).toEqual({
effort: "medium",
summaries: true,
source: "cost_cap",
});
});
it("names the operator's cap when it is the lower of the two", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "high",
admin: { enabled: true, maxEffort: "low" },
costOwner: "operator",
});
expect(r.source).toBe("admin_cap");
expect(r.effort).toBe("low");
});
it("asks for no summaries where the provider gives none", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "medium",
admin: ON,
costOwner: "user",
support: { ...FULL, summaries: false },
});
expect(r.summaries).toBe(false);
});
it("is off for a provider that cannot reason", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "high",
admin: ON,
costOwner: "user",
support: NO_REASONING,
});
expect(r).toEqual({
effort: "off",
summaries: false,
source: "unsupported",
});
});
});
describe("resolveReasoning — background jobs", () => {
it("never reads the Coach preference", () => {
const r = resolveReasoning({
surface: "daily_briefing",
userPref: "off",
admin: ON,
costOwner: "user",
});
expect(r).toEqual({ effort: "medium", summaries: false, source: "job" });
});
it("takes the operator level when the operator pays", () => {
expect(
resolveReasoning({
surface: "daily_briefing",
admin: ON,
costOwner: "operator",
}).effort,
).toBe("low");
expect(
resolveReasoning({
surface: "period_narrative_month",
admin: ON,
costOwner: "user",
}).effort,
).toBe("medium");
expect(
resolveReasoning({
surface: "period_narrative_week",
admin: ON,
costOwner: "user",
}).effort,
).toBe("low");
});
it("never asks a job for summaries", () => {
for (const job of BACKGROUND_REASONING_JOBS) {
expect(
resolveReasoning({
surface: job,
admin: ON,
costOwner: "user",
support: FULL,
}).summaries,
).toBe(false);
}
});
});
describe("the operator's switch and cap win everywhere", () => {
const owners = ["user", "operator"] as const;
const supports = [null, FULL, NO_REASONING];
const prefs = [undefined, ...REASONING_LEVELS];
it("off is off on every surface, for every preference, payer and provider", () => {
let cases = 0;
for (const surface of SURFACES)
for (const userPref of prefs)
for (const costOwner of owners)
for (const support of supports)
for (const maxEffort of REASONING_MAX_EFFORTS) {
const r = resolveReasoning({
surface,
userPref,
admin: { enabled: false, maxEffort },
costOwner,
support,
});
expect(r).toEqual({
effort: "off",
summaries: false,
source: "admin_off",
});
cases++;
}
expect(cases).toBeGreaterThan(500);
});
it("no resolution ever exceeds the operator's cap", () => {
for (const surface of SURFACES)
for (const userPref of prefs)
for (const costOwner of owners)
for (const maxEffort of REASONING_MAX_EFFORTS) {
const r = resolveReasoning({
surface,
userPref,
admin: { enabled: true, maxEffort },
costOwner,
support: FULL,
});
expect(rank(r.effort)).toBeLessThanOrEqual(rank(maxEffort));
if (costOwner === "operator") {
expect(rank(r.effort)).toBeLessThanOrEqual(rank("medium"));
}
}
});
it("a job resolved off under the switch sends nothing at all", () => {
for (const job of BACKGROUND_REASONING_JOBS) {
const r = resolveReasoning({
surface: job,
admin: { enabled: false, maxEffort: "high" },
costOwner: "user",
});
expect(completionReasoning(r, job)).toBeUndefined();
}
});
});
describe("completionReasoning", () => {
it("sends the Coach's off explicitly, so a provider that can switch off does", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "off",
admin: ON,
costOwner: "user",
});
expect(completionReasoning(r, "coach")).toEqual({
effort: "off",
summaries: false,
});
});
it("sends nothing for a provider that cannot reason", () => {
const r = resolveReasoning({
surface: "coach",
userPref: "high",
admin: ON,
costOwner: "user",
support: NO_REASONING,
});
expect(completionReasoning(r, "coach")).toBeUndefined();
});
it("carries a job's level without summaries", () => {
const r = resolveReasoning({
surface: "daily_briefing",
admin: ON,
costOwner: "user",
});
expect(completionReasoning(r, "daily_briefing")).toEqual({
effort: "medium",
summaries: false,
});
});
});
describe("coachReasoningCeiling", () => {
it("is off when switched off, the cap otherwise, medium at most for the operator", () => {
expect(
coachReasoningCeiling({ enabled: false, maxEffort: "high" }, "user"),
).toBe("off");
expect(
coachReasoningCeiling({ enabled: true, maxEffort: "high" }, "user"),
).toBe("high");
expect(
coachReasoningCeiling({ enabled: true, maxEffort: "high" }, "operator"),
).toBe("medium");
expect(
coachReasoningCeiling({ enabled: true, maxEffort: "low" }, "operator"),
).toBe("low");
});
});
describe("parseReasoningMaxEffort", () => {
it("keeps the three caps and reads anything else as no cap", () => {
expect(parseReasoningMaxEffort("low")).toBe("low");
expect(parseReasoningMaxEffort("medium")).toBe("medium");
expect(parseReasoningMaxEffort("off")).toBe("high");
expect(parseReasoningMaxEffort(null)).toBe("high");
});
});
describe("reasoningSupportForChainEntry", () => {
it("reads the client each chain tag builds", () => {
expect(reasoningSupportForChainEntry("codex", {}).effort).toBe(true);
expect(reasoningSupportForChainEntry("admin-codex", {}).effort).toBe(true);
// The OpenAI client's default model does not reason.
expect(reasoningSupportForChainEntry("openai", {}).effort).toBe(false);
expect(
reasoningSupportForChainEntry("openai", { model: "gpt-5" }).offIsReal,
).toBe(false);
expect(
reasoningSupportForChainEntry("anthropic", { model: "claude-opus-5" })
.offIsReal,
).toBe(false);
expect(
reasoningSupportForChainEntry("admin-openai", {
adminIsAnthropic: true,
adminModel: "claude-opus-5",
}).offIsReal,
).toBe(false);
expect(
reasoningSupportForChainEntry("openai-compatible", {
compatModel: "x",
compatBaseUrl: "https://openrouter.ai/api/v1",
}).stateRoundTrip,
).toBe(true);
expect(reasoningSupportForChainEntry("local", {}).effort).toBe(true);
});
});
+176
View File
@@ -0,0 +1,176 @@
/**
* v1.41 — the reads behind a reasoning decision: the operator's two controls,
* and the resolved Coach state the account payload publishes.
*
* Server-only. The decision itself is the pure `resolveReasoning` in
* `./resolve.ts`; this file only gathers its inputs.
*/
import { prisma } from "@/lib/db";
import { annotate, getEvent } from "@/lib/logging/context";
import { memoizePerRequest } from "@/lib/request-cache";
import { isAnthropicBaseUrl, probeProviderChain } from "@/lib/ai/provider";
import { isOperatorFundedProvider } from "@/lib/ai/coach/budget";
import type { ProviderChainType } from "@/lib/ai/provider-chain";
import type { ProviderWorkAuthority } from "@/lib/sharing/provider-work-authority";
import type { CompletionParams } from "@/lib/ai/types";
import {
coachReasoningLevel,
type CoachPrefs,
} from "@/lib/validations/coach-prefs";
import type { BackgroundReasoningJob } from "./levels";
import {
coachReasoningCeiling,
completionReasoning,
parseReasoningMaxEffort,
reasoningSupportForChainEntry,
resolveReasoning,
type CoachReasoningState,
type ReasoningAdminControls,
type ReasoningCostOwner,
} from "./resolve";
/**
* Fails closed: an unreadable settings row reads as "reasoning off", so a
* storage blip can never hand out reasoning the operator switched off. The
* calls it affects still run, without reasoning.
*/
const UNREADABLE: ReasoningAdminControls = Object.freeze({
enabled: false,
maxEffort: "low",
});
/**
* The operator's reasoning switch and cap. Memoised per request; read afresh
* inside a background run, so switching reasoning off at 02:05 stops the
* nightly pass that began at 02:00 for every account after the switch.
*/
export function loadReasoningControls(): Promise<ReasoningAdminControls> {
return memoizePerRequest(
"ai-reasoning-controls",
async () => {
try {
const row = await prisma.appSettings.findUnique({
where: { id: "singleton" },
select: { aiReasoningEnabled: true, aiReasoningMaxEffort: true },
});
return {
enabled: row?.aiReasoningEnabled ?? true,
maxEffort: parseReasoningMaxEffort(row?.aiReasoningMaxEffort),
};
} catch {
getEvent()?.addWarning(
"Failed to load the reasoning controls; reasoning is off for this request",
);
return UNREADABLE;
}
},
{ freshInBackground: true },
);
}
/** Who pays for a chain whose primary is `providerType` (`null`: none). */
function costOwnerOf(providerType: string | undefined): ReasoningCostOwner {
return providerType === undefined ||
isOperatorFundedProvider(providerType as ProviderChainType)
? "operator"
: "user";
}
/**
* Resolve the Coach reasoning state for the account payload. The preference is
* the caller's own (Coach preferences belong to the person); the provider is
* the record's, read through the memoised presence probe the `ai` block
* already ran, plus one point read of the model columns.
*/
export async function loadCoachReasoningState(args: {
prefs: CoachPrefs;
recordId: string;
authority: ProviderWorkAuthority | null;
}): Promise<CoachReasoningState> {
const [admin, presence] = await Promise.all([
loadReasoningControls(),
probeProviderChain(args.recordId, args.authority),
]);
const primary = presence.entries[0]?.providerType as
ProviderChainType | undefined;
const costOwner = costOwnerOf(primary);
let support = null;
if (primary !== undefined) {
const [row, settings] = await Promise.all([
prisma.user.findUnique({
where: { id: args.recordId },
select: { aiModel: true, aiCompatModel: true, aiCompatBaseUrl: true },
}),
primary === "admin-openai"
? prisma.appSettings.findUnique({
where: { id: "singleton" },
select: { adminAiModel: true, adminAiBaseUrl: true },
})
: Promise.resolve(null),
]);
support = reasoningSupportForChainEntry(primary, {
model: row?.aiModel,
compatModel: row?.aiCompatModel,
compatBaseUrl: row?.aiCompatBaseUrl,
adminModel: settings?.adminAiModel,
adminIsAnthropic: settings?.adminAiBaseUrl
? isAnthropicBaseUrl(settings.adminAiBaseUrl)
: false,
});
}
const preference = coachReasoningLevel(args.prefs);
const resolved = resolveReasoning({
surface: "coach",
userPref: preference,
admin,
costOwner,
support,
});
return {
level: resolved.effort,
preference,
maxLevel: coachReasoningCeiling(admin, costOwner),
available: admin.enabled && support !== null && support.effort,
offIsReal: support?.offIsReal ?? true,
source: resolved.source,
};
}
/**
* The reasoning one background call carries, or `undefined` for none.
*
* Only the jobs in `BACKGROUND_REASONING_JOBS` can ask; the operator's switch
* and cap apply like everywhere else, the payer comes from the chain's
* primary, and the provider's ability is left to the client (the model is not
* known here, and every client sends nothing it cannot honour and learns a
* refusal per endpoint). The decision is stamped on the wide event as meta,
* never as an action: the job owns the action name.
*/
export async function resolveJobReasoning(
job: BackgroundReasoningJob,
chain: ReadonlyArray<{ providerType: ProviderChainType }>,
): Promise<CompletionParams["reasoning"]> {
// Reasoning is an improvement to a job, never a precondition for it: any
// failure here sends the call exactly as it went before reasoning existed.
let resolved;
try {
resolved = resolveReasoning({
surface: job,
admin: await loadReasoningControls(),
costOwner: costOwnerOf(chain[0]?.providerType),
});
} catch {
return undefined;
}
annotate({
meta: {
ai_reasoning_job: job,
ai_reasoning_effort: resolved.effort,
ai_reasoning_source: resolved.source,
},
});
return completionReasoning(resolved, job);
}
+260
View File
@@ -0,0 +1,260 @@
/**
* v1.41 — how hard one call may think, decided once per Coach turn or job.
*
* ## The one entry point
*
* resolveReasoning({ surface, userPref, admin, costOwner, support })
* → { effort, summaries, source }
*
* - `surface`: `"coach"` or one of the background jobs in
* `BACKGROUND_REASONING_JOBS`. Nothing else is representable, which is
* the point: a status card, a batch note, an archetype, a nudge or a
* document read has no surface to resolve against, so it cannot ask for
* reasoning by accident.
* - `userPref`: the person's Coach preference (`coachReasoningLevel(prefs)`).
* Read for the Coach only; a job's level comes from
* `BACKGROUND_REASONING_LEVEL`, never from the Coach preference.
* - `admin`: the operator's two controls, `loadReasoningControls()` in
* `./controls.ts` (one memoised read of the settings row).
* - `costOwner`: who pays the call, `resolveCostOwner(chain)` from the Coach
* budget module. An operator-funded chain is held to `medium` at most.
* - `support`: what the chain's primary can do (`reasoningSupport(...)`), or
* `null` when the caller does not know the model; unknown is optimistic,
* because the clients learn a refusal per endpoint (`dialect-cache.ts`).
*
* The order is fixed and every arm wins over the ones after it: the
* operator's switch, the provider's ability, the requested level, the
* operator's cap, the cost cap. `source` names the arm that decided, so a
* dashboard and the settings both say why a level is what it is.
*
* `completionReasoning(resolved, surface)` turns the answer into the
* `CompletionParams.reasoning` a call carries, or `undefined` when the call
* should go out exactly as it did before reasoning existed. Pass the answer
* budget as `maxTokens`; each client adds the thinking budget itself.
*
* A leaf on purpose: no Prisma, no provider client, no `provider-chain.ts`
* value import, so a worker, a route and a test can all call it. The loaders
* live in `./controls.ts`.
*/
import type { ProviderChainType } from "../provider-chain";
import type { CompletionParams } from "../types";
import {
BACKGROUND_REASONING_LEVEL,
DEFAULT_REASONING_LEVEL,
DEFAULT_REASONING_MAX_EFFORT,
REASONING_LEVELS,
REASONING_MAX_EFFORTS,
capReasoningLevel,
type BackgroundReasoningJob,
type ReasoningLevel,
type ReasoningMaxEffort,
} from "./levels";
import { reasoningSupport, type ReasoningSupport } from "./support";
/** Where a resolution is asked for: the Coach, or one admitted job. */
export type ReasoningSurface = "coach" | BackgroundReasoningJob;
/** The operator's two controls on the settings row (migration 0376). */
export interface ReasoningAdminControls {
enabled: boolean;
maxEffort: ReasoningMaxEffort;
}
/** The controls an instance that never touched them has. */
export const DEFAULT_REASONING_ADMIN_CONTROLS: ReasoningAdminControls =
Object.freeze({
enabled: true,
maxEffort: DEFAULT_REASONING_MAX_EFFORT,
});
/**
* Why the level is what it is:
*
* - `user`: the person's Coach preference, unchanged.
* - `job`: the job's own level from the background table, unchanged.
* - `admin_off`: the operator switched reasoning off for the instance.
* - `admin_cap`: lowered to the operator's highest level.
* - `cost_cap`: lowered because the operator pays for the call.
* - `unsupported`: the provider cannot reason at all.
*/
export const REASONING_SOURCES = [
"user",
"job",
"admin_off",
"admin_cap",
"cost_cap",
"unsupported",
] as const;
export type ReasoningSource = (typeof REASONING_SOURCES)[number];
export interface ResolvedReasoning {
effort: ReasoningLevel;
/** Ask the provider for readable summaries (the Coach's live trail only). */
summaries: boolean;
source: ReasoningSource;
}
export type ReasoningCostOwner = "operator" | "user";
export interface ResolveReasoningInput {
surface: ReasoningSurface;
userPref?: ReasoningLevel | null;
admin: ReasoningAdminControls;
costOwner: ReasoningCostOwner;
support?: ReasoningSupport | null;
}
/**
* The Coach reasoning block on `GET /api/auth/me`, resolved on the server
* (`loadCoachReasoningState`) and read by the web settings and the native
* client alike. Client-safe, which is why it lives here.
*/
export interface CoachReasoningState {
/** The level a Coach turn runs at now, every cap applied. */
level: ReasoningLevel;
/** What the person picked (`coachPrefsJson.reasoning`), `medium` when never. */
preference: ReasoningLevel;
/** The highest level the settings may offer; `off` when the operator switched it off. */
maxLevel: ReasoningLevel;
/** False when the operator switched reasoning off or the provider cannot reason. */
available: boolean;
/** False when the provider cannot switch reasoning off (the lowest option reads "Minimal"). */
offIsReal: boolean;
/** Why `level` is what it is. */
source: ReasoningSource;
}
/** The highest level an operator-funded call may take (plan 7.2). */
export const OPERATOR_FUNDED_REASONING_CAP: ReasoningMaxEffort = "medium";
/** Narrow an untrusted settings value onto the cap vocabulary. */
export function parseReasoningMaxEffort(value: unknown): ReasoningMaxEffort {
return typeof value === "string" &&
(REASONING_MAX_EFFORTS as readonly string[]).includes(value)
? (value as ReasoningMaxEffort)
: DEFAULT_REASONING_MAX_EFFORT;
}
function lower(a: ReasoningLevel, b: ReasoningLevel): boolean {
return REASONING_LEVELS.indexOf(a) < REASONING_LEVELS.indexOf(b);
}
export function resolveReasoning(
input: ResolveReasoningInput,
): ResolvedReasoning {
const coach = input.surface === "coach";
if (!input.admin.enabled) {
return { effort: "off", summaries: false, source: "admin_off" };
}
if (input.support && !input.support.effort) {
return { effort: "off", summaries: false, source: "unsupported" };
}
let effort: ReasoningLevel;
let source: ReasoningSource;
if (input.surface === "coach") {
effort = input.userPref ?? DEFAULT_REASONING_LEVEL;
source = "user";
} else {
const levels = BACKGROUND_REASONING_LEVEL[input.surface];
effort =
input.costOwner === "operator" ? levels.operatorLevel : levels.level;
source = "job";
}
const adminCapped = capReasoningLevel(effort, input.admin.maxEffort);
if (lower(adminCapped, effort)) {
effort = adminCapped;
source = "admin_cap";
}
if (input.costOwner === "operator") {
const costCapped = capReasoningLevel(effort, OPERATOR_FUNDED_REASONING_CAP);
if (lower(costCapped, effort)) {
effort = costCapped;
source = "cost_cap";
}
}
return {
effort,
summaries: coach && effort !== "off" && (input.support?.summaries ?? true),
source,
};
}
/**
* The `CompletionParams.reasoning` one call carries for `resolved`.
*
* `undefined` means "send what the call sent before reasoning existed": the
* provider cannot reason, or a background job resolved to `off` (the
* operator's switch included), which a job expresses by not asking. The Coach
* sends an explicit `off` when the person or the operator chose it, so a
* provider that can switch reasoning off really does (`offIsReal`).
*/
export function completionReasoning(
resolved: ResolvedReasoning,
surface: ReasoningSurface,
): CompletionParams["reasoning"] {
if (resolved.source === "unsupported") return undefined;
if (resolved.effort === "off" && surface !== "coach") return undefined;
return { effort: resolved.effort, summaries: resolved.summaries };
}
/**
* The highest level the Coach may take for this person right now: what the
* settings dropdown offers. `off` when the operator switched reasoning off.
*/
export function coachReasoningCeiling(
admin: ReasoningAdminControls,
costOwner: ReasoningCostOwner,
): ReasoningLevel {
if (!admin.enabled) return "off";
return costOwner === "operator"
? capReasoningLevel(admin.maxEffort, OPERATOR_FUNDED_REASONING_CAP)
: admin.maxEffort;
}
/**
* What a chain entry can do with reasoning, from the configuration columns a
* presence read already holds (no client built, no key decrypted). The chain
* tag decides the client: the Codex paths are the Codex client, the person's
* OpenAI key and the operator's slot are the OpenAI client (or Anthropic,
* when the operator's base URL is Anthropic's), and so on. Model names fall
* back to the defaults the resolvers use.
*/
export function reasoningSupportForChainEntry(
providerType: ProviderChainType,
config: {
model?: string | null;
compatModel?: string | null;
compatBaseUrl?: string | null;
adminModel?: string | null;
adminIsAnthropic?: boolean;
},
): ReasoningSupport {
switch (providerType) {
case "codex":
case "admin-codex":
return reasoningSupport("codex", "");
case "openai":
return reasoningSupport("admin-key", config.model ?? "gpt-4o");
case "anthropic":
return reasoningSupport("anthropic", config.model ?? "claude-sonnet-4-6");
case "local":
return reasoningSupport("local", config.model ?? "");
case "openai-compatible":
return reasoningSupport(
"openai-compatible",
config.compatModel ?? config.model ?? "",
config.compatBaseUrl ? { baseUrl: config.compatBaseUrl } : {},
);
case "admin-openai":
return config.adminIsAnthropic
? reasoningSupport(
"anthropic",
config.adminModel ?? "claude-sonnet-4-6",
)
: reasoningSupport("admin-key", config.adminModel ?? "gpt-4o");
}
}