Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
15 changes: 11 additions & 4 deletions coderd/exp_chats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3897,8 +3897,11 @@ func TestCreateChatModelConfig(t *testing.T) {
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Equal(t, "Invalid model config.", sdkErr.Message)
require.Contains(t, sdkErr.Detail, `reasoning_effort.default " HIGH "`)
require.Contains(t, sdkErr.Detail, "must be one of none, minimal, low, medium, high, xhigh, max")
require.Equal(
Comment thread
DanielleMaywood marked this conversation as resolved.
t,
`reasoning_effort.default " HIGH " must be one of none, minimal, low, medium, high, xhigh, max`,
sdkErr.Detail,
)
})

t.Run("ReasoningEffortRejectsDefaultAboveMax", func(t *testing.T) {
Expand Down Expand Up @@ -7029,8 +7032,9 @@ func TestSendMessageQueuesEffectiveModelConfigID(t *testing.T) {
Type: codersdk.ChatInputPartTypeText,
Text: "queue this with model b",
}},
ModelConfigID: ptr.Ref(modelConfigB.ID),
BusyBehavior: codersdk.ChatBusyBehaviorQueue,
ModelConfigID: ptr.Ref(modelConfigB.ID),
ReasoningEffort: ptr.Ref("high"),
Comment thread
DanielleMaywood marked this conversation as resolved.
BusyBehavior: codersdk.ChatBusyBehaviorQueue,
})
require.NoError(t, err)
require.True(t, resp.Queued)
Expand All @@ -7043,10 +7047,13 @@ func TestSendMessageQueuesEffectiveModelConfigID(t *testing.T) {
require.Len(t, queuedMessages, 1)
require.True(t, queuedMessages[0].ModelConfigID.Valid)
require.Equal(t, modelConfigB.ID, queuedMessages[0].ModelConfigID.UUID)
require.True(t, queuedMessages[0].ReasoningEffort.Valid)
require.Equal(t, database.ChatReasoningEffortHigh, queuedMessages[0].ReasoningEffort.ChatReasoningEffort)

storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
require.NoError(t, err)
require.Equal(t, modelConfigA.ID, storedChat.LastModelConfigID)
require.False(t, storedChat.LastReasoningEffort.Valid)
}

func TestQueuedMessageWithoutOverrideCapturesEnqueueTimeModel(t *testing.T) {
Expand Down
3 changes: 1 addition & 2 deletions coderd/x/chatd/chatd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2888,7 +2888,6 @@ type chatMessage struct {
contextLimit int64
totalCostMicros int64
runtimeMs int64
reasoningEffort string
}

type userChatMessage struct {
Expand Down Expand Up @@ -2949,7 +2948,7 @@ func appendMessageFields(
params.CreatedBy = append(params.CreatedBy, msg.createdBy)
params.APIKeyID = append(params.APIKeyID, apiKeyID)
params.ModelConfigID = append(params.ModelConfigID, msg.modelConfigID)
params.ReasoningEffort = append(params.ReasoningEffort, msg.reasoningEffort)
params.ReasoningEffort = append(params.ReasoningEffort, "")
params.Role = append(params.Role, msg.role)
params.Content = append(params.Content, string(msg.content.RawMessage))
params.ContentVersion = append(params.ContentVersion, msg.contentVersion)
Expand Down
64 changes: 64 additions & 0 deletions site/src/pages/AgentsPage/AgentChatPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "#/api/queries/chats";
import { workspaceByIdKey } from "#/api/queries/workspaces";
import type * as TypesGen from "#/api/typesGenerated";
import { MockChatMessage } from "#/testHelpers/chatEntities";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import {
MockGroup,
Expand Down Expand Up @@ -122,6 +123,10 @@ const mockModelConfigs: TypesGen.ChatModelConfig[] = [
model: "gpt-4o",
display_name: "GPT-4o",
is_default: true,
model_config: {
reasoning_effort: { default: "medium", max: "high" },
},
reasoning_efforts: ["low", "medium", "high"],
created_at: "2026-02-18T00:00:00.000Z",
updated_at: "2026-02-18T00:00:00.000Z",
},
Expand Down Expand Up @@ -1189,8 +1194,38 @@ export const WithMessageHistory: Story = {
{ diffUrl: undefined },
),
},
beforeEach: () => {
spyOn(API.experimental, "getChat").mockResolvedValue({
id: CHAT_ID,
...baseChatFields,
title: "Markdown rendering showcase",
status: "waiting",
});
spyOn(API.experimental, "editChatMessage").mockResolvedValue({
message: {
...MockChatMessage,
id: 5,
created_at: "2026-02-18T00:03:00.000Z",
},
});
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);
const user = userEvent.setup();
const changeReasoningEffort = async (key: string) => {
const modelSelector = canvas.getByRole("combobox", { name: "GPT-4o" });
await user.click(modelSelector);
const slider = await body.findByRole("slider");
slider.focus();
await user.keyboard(key);
await user.click(modelSelector);
};
const editLastMessage = async () => {
const buttons = canvas.getAllByRole("button", { name: "Edit message" });
await user.click(buttons[buttons.length - 1]);
};

expect(
await canvas.findByText("Markdown rendering showcase"),
).toBeVisible();
Expand All @@ -1199,6 +1234,35 @@ export const WithMessageHistory: Story = {
canvas.queryByText(/^This chat is owned by/),
).not.toBeInTheDocument();
});

await changeReasoningEffort("{ArrowRight}");
await editLastMessage();
await user.click(canvas.getByRole("button", { name: "Save Edit" }));
await waitFor(() => {
expect(API.experimental.editChatMessage).toHaveBeenCalledTimes(1);
expect(
canvas.getByRole("textbox", { name: "Chat message" }),
).toBeEnabled();
});

await editLastMessage();
await changeReasoningEffort("{ArrowLeft}");
await user.click(canvas.getByRole("button", { name: "Save Edit" }));
await waitFor(() => {
expect(API.experimental.editChatMessage).toHaveBeenCalledTimes(2);
});
expect(API.experimental.editChatMessage).toHaveBeenNthCalledWith(
1,
CHAT_ID,
5,
expect.not.objectContaining({ reasoning_effort: expect.anything() }),
);
expect(API.experimental.editChatMessage).toHaveBeenNthCalledWith(
2,
CHAT_ID,
5,
expect.objectContaining({ reasoning_effort: "medium" }),
);
},
};

Expand Down
34 changes: 33 additions & 1 deletion site/src/pages/AgentsPage/AgentChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import {
resolveModelSelector,
} from "./utils/modelOptions";
import { parsePullRequestUrl } from "./utils/pullRequest";
import { pickReasoningEffort } from "./utils/reasoningEffort";
import {
type ChatDetailError,
formatUsageLimitMessage,
Expand Down Expand Up @@ -720,6 +721,8 @@ const AgentChatPage: FC = () => {
const { organizations, experiments } = useDashboard();
const organizationName = getDefaultOrganizationName(organizations);
const [selectedModel, setSelectedModel] = useState("");
const [selectedReasoningEffort, setSelectedReasoningEffort] = useState("");
const isEditReasoningEffortDirtyRef = useRef(false);
const scrollToBottomRef = useRef<(() => void) | null>(null);
const chatInputRef = useRef<ChatMessageInputRef | null>(null);
const inputValueRef = useRef(
Expand Down Expand Up @@ -1133,6 +1136,17 @@ const AgentChatPage: FC = () => {
return modelOptions[0]?.id ?? "";
})();

const effectiveModelOption = modelOptions.find(
(option) => option.id === effectiveSelectedModel,
);
const effectiveReasoningEffort = effectiveModelOption
? pickReasoningEffort(
selectedReasoningEffort || chatRecord?.last_reasoning_effort,
effectiveModelOption.reasoningEfforts ?? [],
effectiveModelOption.reasoningEffortDefault,
)
: undefined;

const compressionThreshold = resolveCompactionThreshold(
chatLastModelConfigID,
userThresholdsQuery.data?.thresholds,
Expand Down Expand Up @@ -1257,6 +1271,12 @@ const AgentChatPage: FC = () => {
chatInputRef,
inputValueRef,
});
const handleEditUserMessage = (
...args: Parameters<typeof editing.handleEditUserMessage>
) => {
isEditReasoningEffortDirtyRef.current = false;
editing.handleEditUserMessage(...args);
};

const chatTitle = chatQuery.data?.title;

Expand Down Expand Up @@ -1453,9 +1473,13 @@ const AgentChatPage: FC = () => {
pickerModelConfigID !== originalModelConfigID
? pickerModelConfigID
: undefined;
// Omit so the backend preserves the original effort.
const request: TypesGen.EditChatMessageRequest = {
content,
model_config_id: editSelectedModelConfigID,
reasoning_effort: isEditReasoningEffortDirtyRef.current
? effectiveReasoningEffort
: undefined,
};
const optimisticMessage = originalEditedMessage
? buildOptimisticEditedMessage({
Expand Down Expand Up @@ -1498,6 +1522,7 @@ const AgentChatPage: FC = () => {
const request: CreateChatMessageRequestWithClearablePlanMode = {
content,
model_config_id: selectedModelConfigID,
reasoning_effort: effectiveReasoningEffort,
mcp_server_ids:
effectiveMCPServerIds.length > 0
? [...effectiveMCPServerIds]
Expand Down Expand Up @@ -1643,12 +1668,19 @@ const AgentChatPage: FC = () => {
workspaceAgent={workspaceAgent}
chatBuildId={chatQuery.data?.build_id}
store={store}
editing={editing}
editing={{ ...editing, handleEditUserMessage }}
effectiveSelectedModel={effectiveSelectedModel}
setSelectedModel={setSelectedModel}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
modelSelectorHelp={modelSelectorHelp}
reasoningEffort={effectiveReasoningEffort}
onReasoningEffortChange={(value) => {
setSelectedReasoningEffort(value);
if (editing.editingMessageId !== null) {
isEditReasoningEffortDirtyRef.current = true;
}
}}
canConfigureAgentSetup={permissions.editDeploymentConfig}
providerCount={providerCount}
modelCount={modelCount}
Expand Down
6 changes: 6 additions & 0 deletions site/src/pages/AgentsPage/AgentChatPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ interface AgentChatPageViewProps {
modelOptions: readonly ModelSelectorOption[];
modelSelectorPlaceholder: string;
modelSelectorHelp?: ReactNode;
reasoningEffort?: string;
onReasoningEffortChange?: (value: string) => void;
canConfigureAgentSetup: boolean;
providerCount?: number;
modelCount?: number;
Expand Down Expand Up @@ -328,6 +330,8 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
modelOptions,
modelSelectorPlaceholder,
modelSelectorHelp,
reasoningEffort,
onReasoningEffortChange,
canConfigureAgentSetup,
providerCount,
modelCount,
Expand Down Expand Up @@ -941,6 +945,8 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
modelSelectorHelp={modelSelectorHelp}
reasoningEffort={reasoningEffort}
onReasoningEffortChange={onReasoningEffortChange}
planModeEnabled={planModeEnabled}
onPlanModeToggle={onPlanModeToggle}
isModelCatalogLoading={isModelCatalogLoading}
Expand Down
2 changes: 2 additions & 0 deletions site/src/pages/AgentsPage/AgentCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const AgentCreatePage: FC = () => {
fileIDs,
workspaceId,
model,
reasoningEffort,
mcpServerIds,
organizationId,
planMode,
Expand All @@ -110,6 +111,7 @@ const AgentCreatePage: FC = () => {
plan_mode: planMode === "plan" ? "plan" : undefined,
client_type: "ui",
...(model ? { model_config_id: model } : {}),
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
};
const createdChat = await createMutation.mutateAsync(createRequest);

Expand Down
6 changes: 6 additions & 0 deletions site/src/pages/AgentsPage/components/AgentChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ interface AgentChatInputProps {
modelOptions: readonly ModelSelectorOption[];
modelSelectorPlaceholder: string;
hasModelOptions: boolean;
reasoningEffort?: string;
onReasoningEffortChange?: (value: string) => void;
planModeEnabled?: boolean;
onPlanModeToggle?: (enabled: boolean) => void;
isModelCatalogLoading?: boolean;
Expand Down Expand Up @@ -354,6 +356,8 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
modelOptions,
modelSelectorPlaceholder,
hasModelOptions,
reasoningEffort,
onReasoningEffortChange,
planModeEnabled = false,
onPlanModeToggle,
isModelCatalogLoading = false,
Expand Down Expand Up @@ -1428,6 +1432,8 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
dropdownSide="top"
dropdownAlign="start"
enableMobileFullWidthDropdown
reasoningEffort={reasoningEffort}
onReasoningEffortChange={onReasoningEffortChange}
/>
)}
{planModeEnabled && !shouldOverflowPlanningBadge && (
Expand Down
59 changes: 59 additions & 0 deletions site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const getCreateOptions = (onCreateChat: unknown): CreateChatSubmission => {

type CreateChatSubmission = {
model?: string;
reasoningEffort?: string;
};

export const RootPersonalModelOverrideModelSelected: Story = {
Expand Down Expand Up @@ -305,6 +306,64 @@ export const ManualSelectionOverridesRootChatDefault: Story = {
},
};

// Model options with reasoning effort bounds configured. GPT-4o uses the
// full global scale; Claude is capped at medium.
const effortModelOptions = [
{
...modelOptions[0],
reasoningEffortDefault: "medium",
reasoningEfforts: [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
],
},
{
...modelOptions[1],
reasoningEffortDefault: "low",
reasoningEfforts: ["low", "medium"],
},
] as const;

export const SubmitsReasoningEffort: Story = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-10] No story tests submitting without moving the slider (the model default should flow through). The gap is narrow: pickReasoningEffort unit tests cover the fallback from "" to default, and the existing model-only submission stories don't carry reasoning efforts. Worth noting for completeness. (Bisky)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this: default fallback is already covered by focused pickReasoningEffort unit tests, and submission wiring is covered by the form story, so another story would duplicate non-blocking coverage. This reply was generated by Coder Agents.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. Existing coverage is sufficient.

🤖

args: {
...defaultArgs,
onCreateChat: fn().mockResolvedValue(undefined),
modelOptions: [...effortModelOptions],
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);

// Open the model selector; the effort row shows the model default.
await userEvent.click(canvas.getByRole("combobox", { name: "GPT-4o" }));
const slider = await body.findByRole("slider");
// "medium" is the fourth of seven selectable efforts.
expect(slider).toHaveAttribute("aria-valuenow", "3");

// Bump the effort to "high" with the keyboard, then close.
await userEvent.tab();
expect(slider).toHaveFocus();
await userEvent.keyboard("{ArrowRight}");
await waitFor(() => {
expect(slider).toHaveAttribute("aria-valuenow", "4");
});
await userEvent.keyboard("{Escape}");

await submitMessage(canvasElement, "create with reasoning effort");
await waitFor(() => {
expect(args.onCreateChat).toHaveBeenCalled();
});
const options = getCreateOptions(args.onCreateChat);
expect(options.model).toBe(modelConfigID);
expect(options.reasoningEffort).toBe("high");
},
};

const mockWorkspaces = [
{
...MockWorkspace,
Expand Down
Loading
Loading