Skip to content

Commit 4df7f25

Browse files
committed
clamp legacy loop efforts to auto on hydrate
1 parent 2521d7d commit 4df7f25

5 files changed

Lines changed: 61 additions & 13 deletions

File tree

packages/ui/src/features/loops/components/LoopModelFields.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,22 +88,17 @@ export function LoopModelFields({
8888
];
8989
}, [catalog, model]);
9090

91-
// The current effort stays selectable even when the effective model no longer
92-
// supports it (a loop saved under an older default), same as the model list.
91+
// An unsupported saved effort can't reach this select: `normalizeLoopFormValues`
92+
// clamps it to auto when the form hydrates.
9393
const reasoningEffortOptions = useMemo(() => {
94-
const supported = loopSupportedReasoningEfforts(adapter, model);
95-
const efforts =
96-
reasoningEffort && !supported.includes(reasoningEffort)
97-
? [...supported, reasoningEffort]
98-
: supported;
9994
return [
10095
{ value: AUTO_REASONING_VALUE, label: "Auto" },
101-
...efforts.map((effort) => ({
96+
...loopSupportedReasoningEfforts(adapter, model).map((effort) => ({
10297
value: effort,
10398
label: REASONING_EFFORT_LABELS[effort],
10499
})),
105100
];
106-
}, [adapter, model, reasoningEffort]);
101+
}, [adapter, model]);
107102

108103
const clearUnsupportedEffort = (
109104
nextAdapter: LoopSchemas.LoopRuntimeAdapterEnum,

packages/ui/src/features/loops/loopFormTypes.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,39 @@ describe("normalizeLoopFormValues", () => {
163163
const values = validFormValues();
164164
expect(normalizeLoopFormValues(values)).toBe(values);
165165
});
166+
167+
it.each([
168+
{
169+
name: "unpinned claude with an effort the default model lacks",
170+
overrides: { model: "", reasoningEffort: "medium" },
171+
expected: null,
172+
},
173+
{
174+
name: "unpinned claude with an effort the default model supports",
175+
overrides: { model: "", reasoningEffort: "high" },
176+
expected: "high",
177+
},
178+
{
179+
name: "pinned model supporting the effort",
180+
overrides: { model: "claude-sonnet-5", reasoningEffort: "medium" },
181+
expected: "medium",
182+
},
183+
{
184+
name: "unpinned codex with an effort the default model lacks",
185+
overrides: {
186+
runtimeAdapter: "codex",
187+
model: "",
188+
reasoningEffort: "max",
189+
},
190+
expected: null,
191+
},
192+
] as const)(
193+
"clamps an unsupported effort to auto: $name",
194+
({ overrides, expected }) => {
195+
const values = { ...validFormValues(), ...overrides };
196+
expect(normalizeLoopFormValues(values).reasoningEffort).toBe(expected);
197+
},
198+
);
166199
});
167200

168201
describe("formValuesToLoopWrite", () => {

packages/ui/src/features/loops/loopFormTypes.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { LoopSchemas } from "@posthog/api-client/loops";
22
import { systemTimezone } from "@posthog/ui/primitives/timezone";
3+
import { loopSupportedReasoningEfforts } from "./loopModelDefaults";
34

45
/**
56
* A trigger row in the create/edit form. `key` is a client-only stable
@@ -131,10 +132,22 @@ export function emptyLoopFormValues(): LoopFormValues {
131132
export function normalizeLoopFormValues(
132133
values: LoopFormValues,
133134
): LoopFormValues {
134-
if (values.contextTarget && values.visibility !== "team") {
135-
return { ...values, visibility: "team" };
135+
let normalized = values;
136+
if (normalized.contextTarget && normalized.visibility !== "team") {
137+
normalized = { ...normalized, visibility: "team" };
138+
}
139+
// A stored effort can predate a fire-time default change; carrying it into
140+
// the form would make every save 400. It fires as auto anyway, so show that.
141+
if (
142+
normalized.reasoningEffort &&
143+
!loopSupportedReasoningEfforts(
144+
normalized.runtimeAdapter,
145+
normalized.model,
146+
).includes(normalized.reasoningEffort)
147+
) {
148+
normalized = { ...normalized, reasoningEffort: null };
136149
}
137-
return values;
150+
return normalized;
138151
}
139152

140153
export function loopToFormValues(loop: LoopSchemas.Loop): LoopFormValues {

packages/ui/src/features/loops/loopModelDefaults.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ describe("loopSupportedReasoningEfforts", () => {
2727
["codex", "", ["low", "medium", "high"]],
2828
["codex", "gpt-5.5", ["low", "medium", "high", "xhigh"]],
2929
["codex", "gpt-5.6-sol", ["low", "medium", "high", "xhigh", "max"]],
30+
["codex", "gpt-unknown", ["low", "medium", "high"]],
31+
["claude", "claude-unknown", ["low", "medium", "high", "xhigh", "max"]],
3032
] as const)(
3133
"adapter %s with model %j supports %j",
3234
(adapter, model, expected) => {

packages/ui/src/features/loops/loopModelDefaults.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ const EFFORTS_BY_MODEL: Record<string, LoopSchemas.LoopReasoningEffortEnum[]> =
2929
"claude-sonnet-4-6": LOW_TO_HIGH,
3030
"gpt-5": LOW_TO_HIGH,
3131
"gpt-5.5": ["low", "medium", "high", "xhigh"],
32+
"gpt-5.6-sol": ALL_EFFORTS,
33+
"gpt-5.6-terra": ALL_EFFORTS,
34+
"gpt-5.6-luna": ALL_EFFORTS,
3235
};
3336

3437
export function loopEffectiveModel(
@@ -42,5 +45,7 @@ export function loopSupportedReasoningEfforts(
4245
adapter: LoopSchemas.LoopRuntimeAdapterEnum,
4346
model: string,
4447
): LoopSchemas.LoopReasoningEffortEnum[] {
45-
return EFFORTS_BY_MODEL[loopEffectiveModel(adapter, model)] ?? ALL_EFFORTS;
48+
const efforts = EFFORTS_BY_MODEL[loopEffectiveModel(adapter, model)];
49+
if (efforts) return efforts;
50+
return adapter === "codex" ? LOW_TO_HIGH : ALL_EFFORTS;
4651
}

0 commit comments

Comments
 (0)