mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
refactor(ai): move the Codex reasoning wire into its own module
The reasoning-field planning and the refusal learning are a self-contained part of the Codex wire. Moving them out of codex-client.ts leaves room for the per-sign-in model selection that follows without growing a file the size ratchet already holds at its limit. Refs #1287
This commit is contained in:
1 parent
b1fe1f0597
commit
1d374dd4d5
3 files changed
+161
-147
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 84,
|
||||
"total": 94692,
|
||||
"total": 94554,
|
||||
"entries": {
|
||||
"app/medications/page-client.tsx": 839,
|
||||
"components/charts/mood-chart.tsx": 1166,
|
||||
@@ -28,7 +28,7 @@
|
||||
"lib/ai/coach/snapshot.ts": 1346,
|
||||
"lib/ai/coach/stream-events.ts": 817,
|
||||
"lib/ai/coach/types.ts": 1011,
|
||||
"lib/ai/codex-client.ts": 1045,
|
||||
"lib/ai/codex-client.ts": 907,
|
||||
"lib/ai/prompts/insight-generator.ts": 1110,
|
||||
"lib/ai/provider.ts": 1632,
|
||||
"lib/analytics/score/reader.ts": 998,
|
||||
|
||||
+7
-145
@@ -11,14 +11,14 @@ import type {
|
||||
CompletionParams,
|
||||
CompletionResult,
|
||||
} from "./types";
|
||||
import {
|
||||
annotateReasoningDowngrade,
|
||||
isReasoningParameterRejection,
|
||||
learnedReasoningDialect,
|
||||
rememberReasoningDialect,
|
||||
} from "./reasoning/dialect-cache";
|
||||
import type { ReasoningLevel } from "./reasoning/levels";
|
||||
import { levelOfWireEffort, reasoningTitleOf } from "./reasoning/support";
|
||||
import {
|
||||
CODEX_ENDPOINT,
|
||||
asksEncryptedReasoning,
|
||||
learnFromReasoningRejection,
|
||||
planCodexReasoning,
|
||||
type CodexReasoningWire,
|
||||
} from "./codex-reasoning-wire";
|
||||
import {
|
||||
getCachedCodexSlug,
|
||||
invalidateCachedCodexSlug,
|
||||
@@ -54,8 +54,6 @@ export { resolveCodexVisionSlug } from "./codex-slug-chain";
|
||||
* longer bricks the integration.
|
||||
*/
|
||||
|
||||
const CODEX_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
||||
|
||||
/**
|
||||
* Triggers per spec §7b — body / status that mean "this slug is no
|
||||
* longer accepted, walk to the next one in the chain".
|
||||
@@ -207,142 +205,6 @@ function codexStateItems(
|
||||
);
|
||||
}
|
||||
|
||||
// ── v1.41 reasoning wire ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The Responses-API efforts in ascending order. The Codex backend names the
|
||||
* values a slug accepts in its 400 body ("Supported values are: 'none',
|
||||
* 'low', …"), which is how a slug's ladder is learned.
|
||||
*/
|
||||
const CODEX_EFFORT_ORDER = [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
] as const;
|
||||
|
||||
/** What one request sends: `null` is today's `reasoning: null`. */
|
||||
type CodexReasoningWire = { effort: string; summary: boolean } | null;
|
||||
|
||||
/** What a slug was learned to refuse (see `reasoning/dialect-cache.ts`). */
|
||||
interface CodexReasoningDialect {
|
||||
/** The slug refused `reasoning` altogether. */
|
||||
disabled?: true;
|
||||
/** The slug refused `reasoning.summary`. */
|
||||
noSummary?: true;
|
||||
/** The efforts the slug named as supported when it refused one. */
|
||||
efforts?: string[];
|
||||
}
|
||||
|
||||
function dialectKey(slug: string) {
|
||||
return { provider: "codex", endpoint: CODEX_ENDPOINT, model: slug };
|
||||
}
|
||||
|
||||
function readCodexDialect(slug: string): CodexReasoningDialect {
|
||||
const raw = learnedReasoningDialect(dialectKey(slug));
|
||||
if (!raw) return {};
|
||||
try {
|
||||
return JSON.parse(raw) as CodexReasoningDialect;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function wantedEffort(level: ReasoningLevel): string {
|
||||
// "Off" is a real off on the current slugs: a 2026-10-06 probe answered
|
||||
// `effort: "none"` on gpt-5.5 with a non-reasoning reply. A slug that
|
||||
// refuses it learns its lowest supported value instead.
|
||||
return level === "off" ? "none" : level;
|
||||
}
|
||||
|
||||
/** The supported effort closest to `wanted` from below, else the lowest. */
|
||||
function nearestEffort(wanted: string, supported: string[]): string | null {
|
||||
const rank = (e: string) =>
|
||||
(CODEX_EFFORT_ORDER as readonly string[]).indexOf(e);
|
||||
const known = supported
|
||||
.filter((e) => rank(e) >= 0)
|
||||
.sort((a, b) => rank(a) - rank(b));
|
||||
if (known.length === 0) return null;
|
||||
const below = known.filter((e) => rank(e) <= rank(wanted));
|
||||
return below.length > 0 ? below[below.length - 1] : known[0];
|
||||
}
|
||||
|
||||
function planCodexReasoning(
|
||||
reasoning: CompletionParams["reasoning"],
|
||||
slug: string,
|
||||
): CodexReasoningWire {
|
||||
if (!reasoning) return null;
|
||||
const dialect = readCodexDialect(slug);
|
||||
if (dialect.disabled) return null;
|
||||
let effort: string | null = wantedEffort(reasoning.effort);
|
||||
if (dialect.efforts && !dialect.efforts.includes(effort)) {
|
||||
effort = nearestEffort(effort, dialect.efforts);
|
||||
}
|
||||
if (!effort) return null;
|
||||
return {
|
||||
effort,
|
||||
summary: reasoning.summaries && effort !== "none" && !dialect.noSummary,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the request asks for the encrypted reasoning items, the only
|
||||
* reasoning state a `store: false` request can hand to the next round.
|
||||
*/
|
||||
function asksEncryptedReasoning(wire: CodexReasoningWire): boolean {
|
||||
return wire !== null && wire.effort !== "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 400 that refuses a reasoning parameter and learn what the slug
|
||||
* accepts instead. Returns the lesser wire to retry with, or `undefined`
|
||||
* when the body is not about reasoning (the caller's error path takes it).
|
||||
*/
|
||||
function learnFromReasoningRejection(
|
||||
slug: string,
|
||||
wire: Exclude<CodexReasoningWire, null>,
|
||||
status: number,
|
||||
body: string,
|
||||
): CodexReasoningWire | undefined {
|
||||
// Only a 400 that refuses the reasoning parameter counts; a 400 about the
|
||||
// prompt or a tool, and a replay error about a reasoning item handed back
|
||||
// from an earlier round, must not teach the slug anything.
|
||||
if (!isReasoningParameterRejection(status, body, /reasoning/i, slug)) {
|
||||
return undefined;
|
||||
}
|
||||
const dialect = readCodexDialect(slug);
|
||||
let next: CodexReasoningWire;
|
||||
if (wire.summary && /reasoning\.summary|summary/i.test(body)) {
|
||||
dialect.noSummary = true;
|
||||
next = { effort: wire.effort, summary: false };
|
||||
} else {
|
||||
const listed = /supported values are:([^"\n]*)/i.exec(body);
|
||||
const efforts = listed
|
||||
? Array.from(listed[1].matchAll(/'([a-z]+)'/g), (m) => m[1])
|
||||
: [];
|
||||
const nearest =
|
||||
efforts.length > 0 && !efforts.includes(wire.effort)
|
||||
? nearestEffort(wire.effort, efforts)
|
||||
: null;
|
||||
if (nearest) {
|
||||
dialect.efforts = efforts;
|
||||
next = { effort: nearest, summary: wire.summary && nearest !== "none" };
|
||||
} else {
|
||||
dialect.disabled = true;
|
||||
next = null;
|
||||
}
|
||||
}
|
||||
rememberReasoningDialect(dialectKey(slug), JSON.stringify(dialect));
|
||||
annotateReasoningDowngrade(
|
||||
"codex",
|
||||
wire.effort,
|
||||
next ? next.effort : "default",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map tool defs into the Responses `function` tool shape.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* The reasoning half of the Codex wire: what `reasoning` field a request
|
||||
* sends for a requested level, and what a slug that refuses it is asked
|
||||
* instead. A slug's refusals are learned per slug in the shared dialect cache
|
||||
* (`reasoning/dialect-cache.ts`), so the next request does not pay the
|
||||
* refused round-trip again.
|
||||
*/
|
||||
import {
|
||||
annotateReasoningDowngrade,
|
||||
isReasoningParameterRejection,
|
||||
learnedReasoningDialect,
|
||||
rememberReasoningDialect,
|
||||
} from "./reasoning/dialect-cache";
|
||||
import type { ReasoningLevel } from "./reasoning/levels";
|
||||
import type { CompletionParams } from "./types";
|
||||
|
||||
/** The responses endpoint, and the key its learned dialects are kept under. */
|
||||
export const CODEX_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
||||
|
||||
/**
|
||||
* The Responses-API efforts in ascending order. The Codex backend names the
|
||||
* values a slug accepts in its 400 body ("Supported values are: 'none',
|
||||
* 'low', …"), which is how a slug's ladder is learned.
|
||||
*/
|
||||
const CODEX_EFFORT_ORDER = [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
] as const;
|
||||
|
||||
/** What one request sends: `null` is today's `reasoning: null`. */
|
||||
export type CodexReasoningWire = { effort: string; summary: boolean } | null;
|
||||
|
||||
/** What a slug was learned to refuse (see `reasoning/dialect-cache.ts`). */
|
||||
interface CodexReasoningDialect {
|
||||
/** The slug refused `reasoning` altogether. */
|
||||
disabled?: true;
|
||||
/** The slug refused `reasoning.summary`. */
|
||||
noSummary?: true;
|
||||
/** The efforts the slug named as supported when it refused one. */
|
||||
efforts?: string[];
|
||||
}
|
||||
|
||||
function dialectKey(slug: string) {
|
||||
return { provider: "codex", endpoint: CODEX_ENDPOINT, model: slug };
|
||||
}
|
||||
|
||||
function readCodexDialect(slug: string): CodexReasoningDialect {
|
||||
const raw = learnedReasoningDialect(dialectKey(slug));
|
||||
if (!raw) return {};
|
||||
try {
|
||||
return JSON.parse(raw) as CodexReasoningDialect;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function wantedEffort(level: ReasoningLevel): string {
|
||||
// "Off" is a real off on the current slugs: a 2026-10-06 probe answered
|
||||
// `effort: "none"` on gpt-5.5 with a non-reasoning reply. A slug that
|
||||
// refuses it learns its lowest supported value instead.
|
||||
return level === "off" ? "none" : level;
|
||||
}
|
||||
|
||||
/** The supported effort closest to `wanted` from below, else the lowest. */
|
||||
function nearestEffort(wanted: string, supported: string[]): string | null {
|
||||
const rank = (e: string) =>
|
||||
(CODEX_EFFORT_ORDER as readonly string[]).indexOf(e);
|
||||
const known = supported
|
||||
.filter((e) => rank(e) >= 0)
|
||||
.sort((a, b) => rank(a) - rank(b));
|
||||
if (known.length === 0) return null;
|
||||
const below = known.filter((e) => rank(e) <= rank(wanted));
|
||||
return below.length > 0 ? below[below.length - 1] : known[0];
|
||||
}
|
||||
|
||||
export function planCodexReasoning(
|
||||
reasoning: CompletionParams["reasoning"],
|
||||
slug: string,
|
||||
): CodexReasoningWire {
|
||||
if (!reasoning) return null;
|
||||
const dialect = readCodexDialect(slug);
|
||||
if (dialect.disabled) return null;
|
||||
let effort: string | null = wantedEffort(reasoning.effort);
|
||||
if (dialect.efforts && !dialect.efforts.includes(effort)) {
|
||||
effort = nearestEffort(effort, dialect.efforts);
|
||||
}
|
||||
if (!effort) return null;
|
||||
return {
|
||||
effort,
|
||||
summary: reasoning.summaries && effort !== "none" && !dialect.noSummary,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the request asks for the encrypted reasoning items, the only
|
||||
* reasoning state a `store: false` request can hand to the next round.
|
||||
*/
|
||||
export function asksEncryptedReasoning(wire: CodexReasoningWire): boolean {
|
||||
return wire !== null && wire.effort !== "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 400 that refuses a reasoning parameter and learn what the slug
|
||||
* accepts instead. Returns the lesser wire to retry with, or `undefined`
|
||||
* when the body is not about reasoning (the caller's error path takes it).
|
||||
*/
|
||||
export function learnFromReasoningRejection(
|
||||
slug: string,
|
||||
wire: Exclude<CodexReasoningWire, null>,
|
||||
status: number,
|
||||
body: string,
|
||||
): CodexReasoningWire | undefined {
|
||||
// Only a 400 that refuses the reasoning parameter counts; a 400 about the
|
||||
// prompt or a tool, and a replay error about a reasoning item handed back
|
||||
// from an earlier round, must not teach the slug anything.
|
||||
if (!isReasoningParameterRejection(status, body, /reasoning/i, slug)) {
|
||||
return undefined;
|
||||
}
|
||||
const dialect = readCodexDialect(slug);
|
||||
let next: CodexReasoningWire;
|
||||
if (wire.summary && /reasoning\.summary|summary/i.test(body)) {
|
||||
dialect.noSummary = true;
|
||||
next = { effort: wire.effort, summary: false };
|
||||
} else {
|
||||
const listed = /supported values are:([^"\n]*)/i.exec(body);
|
||||
const efforts = listed
|
||||
? Array.from(listed[1].matchAll(/'([a-z]+)'/g), (m) => m[1])
|
||||
: [];
|
||||
const nearest =
|
||||
efforts.length > 0 && !efforts.includes(wire.effort)
|
||||
? nearestEffort(wire.effort, efforts)
|
||||
: null;
|
||||
if (nearest) {
|
||||
dialect.efforts = efforts;
|
||||
next = { effort: nearest, summary: wire.summary && nearest !== "none" };
|
||||
} else {
|
||||
dialect.disabled = true;
|
||||
next = null;
|
||||
}
|
||||
}
|
||||
rememberReasoningDialect(dialectKey(slug), JSON.stringify(dialect));
|
||||
annotateReasoningDowngrade(
|
||||
"codex",
|
||||
wire.effort,
|
||||
next ? next.effort : "default",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
Reference in new issue
Block a user