Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/ui/src/features/loops/components/LoopDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
import { Flex, Text } from "@radix-ui/themes";
import { useRef, useState } from "react";
import { useLoop } from "../hooks/useLoop";
import { useLoopDisplayModel } from "../hooks/useLoopDisplayModel";
import {
useDeleteLoop,
useRunLoop,
Expand All @@ -45,6 +44,7 @@ import {
nextScheduleRun,
summarizeNotificationDestinations,
} from "../loopDisplay";
import { formatLoopModel } from "../loopModels";
import { LoopLoadError } from "./LoopFallbacks";
import { LoopRunRow } from "./LoopRunRow";

Expand Down Expand Up @@ -320,7 +320,7 @@ function PausedNotice({ loop }: { loop: LoopSchemas.Loop }) {
}

function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
const displayModel = useLoopDisplayModel(loop.runtime_adapter, loop.model);
const displayModel = formatLoopModel(loop.runtime_adapter, loop.model);
const {
members,
isLoading: membersLoading,
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/src/features/loops/components/LoopForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
loopToFormValues,
normalizeLoopFormValues,
} from "../loopFormTypes";
import { formatLoopModel } from "../loopModels";
import { LoopBehaviorFields } from "./LoopBehaviorFields";
import { LoopContextFields } from "./LoopContextFields";
import { Field } from "./LoopFormPrimitives";
Expand Down Expand Up @@ -548,9 +549,10 @@ function ReviewList({
/>
<ReviewRow
label="Model"
value={`${ADAPTER_LABELS[values.runtimeAdapter]} · ${
values.model || "Default model"
} · ${reasoning} reasoning`}
value={`${ADAPTER_LABELS[values.runtimeAdapter]} · ${formatLoopModel(
values.runtimeAdapter,
values.model,
)} · ${reasoning} reasoning`}
/>
{showContext ? (
<ReviewRow
Expand Down
105 changes: 56 additions & 49 deletions packages/ui/src/features/loops/components/LoopModelFields.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type { LoopSchemas } from "@posthog/api-client/loops";
import { useModelCatalog } from "@posthog/ui/features/agent-applications/hooks/useModelCatalog";
import { GLM_MODEL_FLAG } from "@posthog/shared";
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
import { SettingsOptionSelect } from "@posthog/ui/features/settings/SettingsOptionSelect";
import { Flex } from "@radix-ui/themes";
import { useMemo } from "react";
import { useLoopModelConfigOptions } from "../hooks/useLoopModelConfigOptions";
import {
clampLoopReasoningEffort,
loopModelOptions,
loopReasoningEffortOptions,
} from "../loopModels";
import { Field } from "./LoopFormPrimitives";

const ADAPTER_OPTIONS: {
Expand All @@ -16,18 +23,6 @@ const ADAPTER_OPTIONS: {
const AUTO_REASONING_VALUE = "auto";
const DEFAULT_MODEL_VALUE = "__default__";

const REASONING_EFFORT_OPTIONS: {
value: LoopSchemas.LoopReasoningEffortEnum | typeof AUTO_REASONING_VALUE;
label: string;
}[] = [
{ value: AUTO_REASONING_VALUE, label: "Auto" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "xhigh", label: "Extra high" },
{ value: "max", label: "Max" },
];

interface LoopModelFieldsProps {
adapter: LoopSchemas.LoopRuntimeAdapterEnum;
model: string;
Expand All @@ -44,9 +39,11 @@ interface LoopModelFieldsProps {
* Static model configuration for a loop: model, adapter, and reasoning effort.
* Loops have no live agent session, so the interactive
* `UnifiedModelSelector`/`ReasoningLevelSelector` (which read a session's
* `SessionConfigOption`) don't apply here, so this presents the same choices
* as a dropdown against the served model catalog instead. The server validates
* the final value against the catalog in `process_task/utils.py`.
* `SessionConfigOption`) don't apply here; instead this presents the same
* per-adapter choices as the main create-task picker (see `loopModels.ts`),
* so every selectable combo passes the server's validation in
* `process_task/utils.py`. Adapter and model switches clamp a now-unsupported
* reasoning effort back to Auto for the same reason.
*/
export function LoopModelFields({
adapter,
Expand All @@ -57,34 +54,47 @@ export function LoopModelFields({
onReasoningEffortChange,
disabled,
}: LoopModelFieldsProps) {
const { catalog } = useModelCatalog();
const glmEnabled = useFeatureFlag(GLM_MODEL_FLAG);
const configOptions = useLoopModelConfigOptions(adapter);

// Prefer the served catalog; fall back to the known level models while it
// loads or if the endpoint is down. Always keep the current value selectable
// so an existing loop's model never drops out of the list.
const modelOptions = useMemo(() => {
const ids = new Set<string>();
for (const entry of catalog.models) {
ids.add(entry.model);
}
if (ids.size === 0) {
for (const level of Object.values(catalog.levels)) {
for (const id of level) {
ids.add(id);
}
}
}
if (model) {
ids.add(model);
}
const catalogOptions = Array.from(ids)
.sort((a, b) => a.localeCompare(b))
.map((id) => ({ value: id, label: id }));
return [
const modelOptions = useMemo(
() => [
{ value: DEFAULT_MODEL_VALUE, label: "Default (recommended)" },
...catalogOptions,
];
}, [catalog, model]);
...loopModelOptions(adapter, configOptions, {
glmEnabled,
pinnedModel: model,
}),
],
[adapter, configOptions, glmEnabled, model],
);

const reasoningOptions = useMemo(
() => [
{ value: AUTO_REASONING_VALUE, label: "Auto" },
...loopReasoningEffortOptions(adapter, model),
],
[adapter, model],
);

const handleAdapterChange = (value: string) => {
const nextAdapter = value as LoopSchemas.LoopRuntimeAdapterEnum;
onAdapterChange(nextAdapter);
// Adapters have disjoint model catalogs, so a pinned model can't carry over.
if (model) onModelChange("");
const clamped = clampLoopReasoningEffort(nextAdapter, "", reasoningEffort);
if (clamped !== reasoningEffort) onReasoningEffortChange(clamped);
};

const handleModelChange = (value: string) => {
const nextModel = value === DEFAULT_MODEL_VALUE ? "" : value;
onModelChange(nextModel);
const clamped = clampLoopReasoningEffort(
adapter,
nextModel,
reasoningEffort,
);
if (clamped !== reasoningEffort) onReasoningEffortChange(clamped);
};

return (
<Flex direction="column" gap="4">
Expand All @@ -96,9 +106,7 @@ export function LoopModelFields({
value={model || DEFAULT_MODEL_VALUE}
options={modelOptions}
placeholder="Default (recommended)"
onValueChange={(value) =>
onModelChange(value === DEFAULT_MODEL_VALUE ? "" : value)
}
onValueChange={handleModelChange}
disabled={disabled}
size="lg"
ariaLabel="Model"
Expand All @@ -110,9 +118,7 @@ export function LoopModelFields({
<SettingsOptionSelect
value={adapter}
options={ADAPTER_OPTIONS}
onValueChange={(value) =>
onAdapterChange(value as LoopSchemas.LoopRuntimeAdapterEnum)
}
onValueChange={handleAdapterChange}
disabled={disabled}
size="lg"
ariaLabel="Adapter"
Expand All @@ -122,7 +128,8 @@ export function LoopModelFields({
<Field label="Reasoning effort" className="min-w-[180px] flex-1">
<SettingsOptionSelect
value={reasoningEffort ?? AUTO_REASONING_VALUE}
options={REASONING_EFFORT_OPTIONS}
options={reasoningOptions}
placeholder="Auto"
onValueChange={(value) =>
onReasoningEffortChange(
value === AUTO_REASONING_VALUE
Expand Down
31 changes: 0 additions & 31 deletions packages/ui/src/features/loops/hooks/useLoopDisplayModel.ts

This file was deleted.

31 changes: 31 additions & 0 deletions packages/ui/src/features/loops/hooks/useLoopModelConfigOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import type { LoopSchemas } from "@posthog/api-client/loops";
import { useHostTRPCClient } from "@posthog/host-router/react";
import { getCloudUrlFromRegion } from "@posthog/shared";
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
import { useQuery } from "@tanstack/react-query";

/**
* The per-adapter session config options (models, efforts) the main
* create-task picker is built from, for reuse by the loop form's static
* pickers. No agent session is created.
*/
export function useLoopModelConfigOptions(
adapter: LoopSchemas.LoopRuntimeAdapterEnum,
): SessionConfigOption[] {
const hostClient = useHostTRPCClient();
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
const { data } = useQuery({
queryKey: ["loops", "model-config-options", cloudRegion, adapter],
queryFn: ({ signal }) => {
if (!cloudRegion) return [];
return hostClient.agent.getPreviewConfigOptions.query(
{ apiHost: getCloudUrlFromRegion(cloudRegion), adapter },
{ signal },
);
},
enabled: !!cloudRegion,
staleTime: 5 * 60_000,
});
return data ?? [];
Comment thread
charlesvien marked this conversation as resolved.
}
Loading
Loading