Skip to content

Commit f969fec

Browse files
authored
fix(loops): source model picker from per-adapter task catalog (#3782)
1 parent 3792cce commit f969fec

7 files changed

Lines changed: 483 additions & 85 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
import { Flex, Text } from "@radix-ui/themes";
3030
import { useRef, useState } from "react";
3131
import { useLoop } from "../hooks/useLoop";
32-
import { useLoopDisplayModel } from "../hooks/useLoopDisplayModel";
3332
import {
3433
useDeleteLoop,
3534
useRunLoop,
@@ -45,6 +44,7 @@ import {
4544
nextScheduleRun,
4645
summarizeNotificationDestinations,
4746
} from "../loopDisplay";
47+
import { formatLoopModel } from "../loopModels";
4848
import { LoopLoadError } from "./LoopFallbacks";
4949
import { LoopRunRow } from "./LoopRunRow";
5050

@@ -320,7 +320,7 @@ function PausedNotice({ loop }: { loop: LoopSchemas.Loop }) {
320320
}
321321

322322
function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
323-
const displayModel = useLoopDisplayModel(loop.runtime_adapter, loop.model);
323+
const displayModel = formatLoopModel(loop.runtime_adapter, loop.model);
324324
const {
325325
members,
326326
isLoading: membersLoading,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
loopToFormValues,
3434
normalizeLoopFormValues,
3535
} from "../loopFormTypes";
36+
import { formatLoopModel } from "../loopModels";
3637
import { LoopBehaviorFields } from "./LoopBehaviorFields";
3738
import { LoopContextFields } from "./LoopContextFields";
3839
import { Field } from "./LoopFormPrimitives";
@@ -548,9 +549,10 @@ function ReviewList({
548549
/>
549550
<ReviewRow
550551
label="Model"
551-
value={`${ADAPTER_LABELS[values.runtimeAdapter]} · ${
552-
values.model || "Default model"
553-
} · ${reasoning} reasoning`}
552+
value={`${ADAPTER_LABELS[values.runtimeAdapter]} · ${formatLoopModel(
553+
values.runtimeAdapter,
554+
values.model,
555+
)} · ${reasoning} reasoning`}
554556
/>
555557
{showContext ? (
556558
<ReviewRow

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

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import type { LoopSchemas } from "@posthog/api-client/loops";
2-
import { useModelCatalog } from "@posthog/ui/features/agent-applications/hooks/useModelCatalog";
2+
import { GLM_MODEL_FLAG } from "@posthog/shared";
3+
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
34
import { SettingsOptionSelect } from "@posthog/ui/features/settings/SettingsOptionSelect";
45
import { Flex } from "@radix-ui/themes";
56
import { useMemo } from "react";
7+
import { useLoopModelConfigOptions } from "../hooks/useLoopModelConfigOptions";
8+
import {
9+
clampLoopReasoningEffort,
10+
loopModelOptions,
11+
loopReasoningEffortOptions,
12+
} from "../loopModels";
613
import { Field } from "./LoopFormPrimitives";
714

815
const ADAPTER_OPTIONS: {
@@ -16,18 +23,6 @@ const ADAPTER_OPTIONS: {
1623
const AUTO_REASONING_VALUE = "auto";
1724
const DEFAULT_MODEL_VALUE = "__default__";
1825

19-
const REASONING_EFFORT_OPTIONS: {
20-
value: LoopSchemas.LoopReasoningEffortEnum | typeof AUTO_REASONING_VALUE;
21-
label: string;
22-
}[] = [
23-
{ value: AUTO_REASONING_VALUE, label: "Auto" },
24-
{ value: "low", label: "Low" },
25-
{ value: "medium", label: "Medium" },
26-
{ value: "high", label: "High" },
27-
{ value: "xhigh", label: "Extra high" },
28-
{ value: "max", label: "Max" },
29-
];
30-
3126
interface LoopModelFieldsProps {
3227
adapter: LoopSchemas.LoopRuntimeAdapterEnum;
3328
model: string;
@@ -44,9 +39,11 @@ interface LoopModelFieldsProps {
4439
* Static model configuration for a loop: model, adapter, and reasoning effort.
4540
* Loops have no live agent session, so the interactive
4641
* `UnifiedModelSelector`/`ReasoningLevelSelector` (which read a session's
47-
* `SessionConfigOption`) don't apply here, so this presents the same choices
48-
* as a dropdown against the served model catalog instead. The server validates
49-
* the final value against the catalog in `process_task/utils.py`.
42+
* `SessionConfigOption`) don't apply here; instead this presents the same
43+
* per-adapter choices as the main create-task picker (see `loopModels.ts`),
44+
* so every selectable combo passes the server's validation in
45+
* `process_task/utils.py`. Adapter and model switches clamp a now-unsupported
46+
* reasoning effort back to Auto for the same reason.
5047
*/
5148
export function LoopModelFields({
5249
adapter,
@@ -57,34 +54,47 @@ export function LoopModelFields({
5754
onReasoningEffortChange,
5855
disabled,
5956
}: LoopModelFieldsProps) {
60-
const { catalog } = useModelCatalog();
57+
const glmEnabled = useFeatureFlag(GLM_MODEL_FLAG);
58+
const configOptions = useLoopModelConfigOptions(adapter);
6159

62-
// Prefer the served catalog; fall back to the known level models while it
63-
// loads or if the endpoint is down. Always keep the current value selectable
64-
// so an existing loop's model never drops out of the list.
65-
const modelOptions = useMemo(() => {
66-
const ids = new Set<string>();
67-
for (const entry of catalog.models) {
68-
ids.add(entry.model);
69-
}
70-
if (ids.size === 0) {
71-
for (const level of Object.values(catalog.levels)) {
72-
for (const id of level) {
73-
ids.add(id);
74-
}
75-
}
76-
}
77-
if (model) {
78-
ids.add(model);
79-
}
80-
const catalogOptions = Array.from(ids)
81-
.sort((a, b) => a.localeCompare(b))
82-
.map((id) => ({ value: id, label: id }));
83-
return [
60+
const modelOptions = useMemo(
61+
() => [
8462
{ value: DEFAULT_MODEL_VALUE, label: "Default (recommended)" },
85-
...catalogOptions,
86-
];
87-
}, [catalog, model]);
63+
...loopModelOptions(adapter, configOptions, {
64+
glmEnabled,
65+
pinnedModel: model,
66+
}),
67+
],
68+
[adapter, configOptions, glmEnabled, model],
69+
);
70+
71+
const reasoningOptions = useMemo(
72+
() => [
73+
{ value: AUTO_REASONING_VALUE, label: "Auto" },
74+
...loopReasoningEffortOptions(adapter, model),
75+
],
76+
[adapter, model],
77+
);
78+
79+
const handleAdapterChange = (value: string) => {
80+
const nextAdapter = value as LoopSchemas.LoopRuntimeAdapterEnum;
81+
onAdapterChange(nextAdapter);
82+
// Adapters have disjoint model catalogs, so a pinned model can't carry over.
83+
if (model) onModelChange("");
84+
const clamped = clampLoopReasoningEffort(nextAdapter, "", reasoningEffort);
85+
if (clamped !== reasoningEffort) onReasoningEffortChange(clamped);
86+
};
87+
88+
const handleModelChange = (value: string) => {
89+
const nextModel = value === DEFAULT_MODEL_VALUE ? "" : value;
90+
onModelChange(nextModel);
91+
const clamped = clampLoopReasoningEffort(
92+
adapter,
93+
nextModel,
94+
reasoningEffort,
95+
);
96+
if (clamped !== reasoningEffort) onReasoningEffortChange(clamped);
97+
};
8898

8999
return (
90100
<Flex direction="column" gap="4">
@@ -96,9 +106,7 @@ export function LoopModelFields({
96106
value={model || DEFAULT_MODEL_VALUE}
97107
options={modelOptions}
98108
placeholder="Default (recommended)"
99-
onValueChange={(value) =>
100-
onModelChange(value === DEFAULT_MODEL_VALUE ? "" : value)
101-
}
109+
onValueChange={handleModelChange}
102110
disabled={disabled}
103111
size="lg"
104112
ariaLabel="Model"
@@ -110,9 +118,7 @@ export function LoopModelFields({
110118
<SettingsOptionSelect
111119
value={adapter}
112120
options={ADAPTER_OPTIONS}
113-
onValueChange={(value) =>
114-
onAdapterChange(value as LoopSchemas.LoopRuntimeAdapterEnum)
115-
}
121+
onValueChange={handleAdapterChange}
116122
disabled={disabled}
117123
size="lg"
118124
ariaLabel="Adapter"
@@ -122,7 +128,8 @@ export function LoopModelFields({
122128
<Field label="Reasoning effort" className="min-w-[180px] flex-1">
123129
<SettingsOptionSelect
124130
value={reasoningEffort ?? AUTO_REASONING_VALUE}
125-
options={REASONING_EFFORT_OPTIONS}
131+
options={reasoningOptions}
132+
placeholder="Auto"
126133
onValueChange={(value) =>
127134
onReasoningEffortChange(
128135
value === AUTO_REASONING_VALUE

packages/ui/src/features/loops/hooks/useLoopDisplayModel.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
2+
import type { LoopSchemas } from "@posthog/api-client/loops";
3+
import { useHostTRPCClient } from "@posthog/host-router/react";
4+
import { getCloudUrlFromRegion } from "@posthog/shared";
5+
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
6+
import { useQuery } from "@tanstack/react-query";
7+
8+
/**
9+
* The per-adapter session config options (models, efforts) the main
10+
* create-task picker is built from, for reuse by the loop form's static
11+
* pickers. No agent session is created.
12+
*/
13+
export function useLoopModelConfigOptions(
14+
adapter: LoopSchemas.LoopRuntimeAdapterEnum,
15+
): SessionConfigOption[] {
16+
const hostClient = useHostTRPCClient();
17+
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
18+
const { data } = useQuery({
19+
queryKey: ["loops", "model-config-options", cloudRegion, adapter],
20+
queryFn: ({ signal }) => {
21+
if (!cloudRegion) return [];
22+
return hostClient.agent.getPreviewConfigOptions.query(
23+
{ apiHost: getCloudUrlFromRegion(cloudRegion), adapter },
24+
{ signal },
25+
);
26+
},
27+
enabled: !!cloudRegion,
28+
staleTime: 5 * 60_000,
29+
});
30+
return data ?? [];
31+
}

0 commit comments

Comments
 (0)