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
34 changes: 34 additions & 0 deletions apps/server/src/provider/providerCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@ const noCapabilities = null;

export const BUILT_IN_PROVIDER_MODELS: Record<ProviderKind, ReadonlyArray<ProviderCatalogEntry>> = {
codex: [
{
slug: "gpt-5.5",
name: "GPT-5.5",
capabilities: {
reasoningEffortLevels: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High", isDefault: true },
{ value: "xhigh", label: "Extra High" },
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
{
slug: "gpt-5.5-mini",
name: "GPT-5.5 Mini",
capabilities: {
reasoningEffortLevels: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High", isDefault: true },
{ value: "xhigh", label: "Extra High" },
],
supportsFastMode: true,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
},
},
{
slug: "gpt-5.4",
name: "GPT-5.4",
Expand Down Expand Up @@ -117,6 +149,8 @@ export const BUILT_IN_PROVIDER_MODELS: Record<ProviderKind, ReadonlyArray<Provid
],
openclaw: [],
copilot: [
{ slug: "gpt-5.5", name: "GPT-5.5", capabilities: noCapabilities },
{ slug: "gpt-5.5-mini", name: "GPT-5.5 Mini", capabilities: noCapabilities },
{ slug: "gpt-5.4", name: "GPT-5.4", capabilities: noCapabilities },
{ slug: "gpt-5.4-mini", name: "GPT-5.4 Mini", capabilities: noCapabilities },
{ slug: "gpt-5.3-codex", name: "GPT-5.3 Codex", capabilities: noCapabilities },
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -939,15 +939,17 @@ export default function ChatView({
],
);
const selectedModel = getModelSelectionModel(selectedModelSelection);
const codexBackendId = serverConfigQuery.data?.codexConfig?.selectedModelProviderId ?? null;
const composerProviderState = useMemo(
() =>
getComposerProviderState({
provider: selectedProvider,
model: selectedModel,
prompt,
modelOptions: draftModelOptions,
codexBackendId,
}),
[draftModelOptions, prompt, selectedModel, selectedProvider],
[codexBackendId, draftModelOptions, prompt, selectedModel, selectedProvider],
);
const selectedPromptEffort = composerProviderState.promptEffort;
const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch;
Expand Down Expand Up @@ -4407,12 +4409,14 @@ export default function ChatView({
threadId,
model: selectedModel,
onPromptChange: setPromptFromTraits,
codexBackendId,
});
const providerTraitsPicker = renderProviderTraitsPicker({
provider: selectedProvider,
threadId,
model: selectedModel,
onPromptChange: setPromptFromTraits,
codexBackendId,
});
const onEnvModeChange = useCallback(
(mode: DraftThreadEnvMode) => {
Expand Down Expand Up @@ -5512,6 +5516,10 @@ export default function ChatView({
planSidebarOpen={planSidebarOpen}
runtimeMode={runtimeMode}
traitsMenuContent={providerTraitsMenuContent}
promptEnhancement={composerPromptEnhancement}
promptEnhancementAvailable={pendingUserInputs.length === 0}
promptEnhancementBusy={isEnhancingPrompt}
onPromptEnhancementChange={onPromptEnhancementChange}
onInteractionModeChange={handleInteractionModeChange}
onTogglePlanSidebar={togglePlanSidebar}
onToggleRuntimeMode={toggleRuntimeMode}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/chat/ClaudeTraitsPicker.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,27 @@ describe("ClaudeTraitsPicker", () => {
}
});

it("checks the Ultrathink option when ultrathink is active in the prompt", async () => {
const mounted = await mountPicker({
model: "claude-opus-4-6",
prompt: "Ultrathink:\nInvestigate this",
});

try {
await page.getByRole("button").click();

const ultrathinkItem = page.getByRole("menuitemradio", { name: "Ultrathink" });
await vi.waitFor(() => {
expect(ultrathinkItem.element().getAttribute("aria-checked")).toBe("true");
});

const highItem = page.getByRole("menuitemradio", { name: "High (default)" });
expect(highItem.element().getAttribute("aria-checked")).toBe("false");
} finally {
await mounted.cleanup();
}
});

it("persists sticky claude model options when traits change", async () => {
const mounted = await mountPicker({
model: "claude-opus-4-6",
Expand Down
16 changes: 12 additions & 4 deletions apps/web/src/components/chat/ClaudeTraitsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ function getSelectedClaudeTraits(
prompt: string,
modelOptions: ClaudeModelOptions | null | undefined,
): {
effort: Exclude<ClaudeCodeEffort, "ultrathink"> | null;
/**
* The effort value to mirror in the radio group. When ultrathink is currently
* active (because the prompt carries the `ultrathink` keyword), this is
* `"ultrathink"` so that the radio group draws a checkmark next to the
* Ultrathink option, matching how every other thinking level renders.
*/
effort: ClaudeCodeEffort | null;
thinkingEnabled: boolean | null;
fastModeEnabled: boolean;
options: ReadonlyArray<ClaudeCodeEffort>;
Expand All @@ -59,7 +65,7 @@ function getSelectedClaudeTraits(
"ultrathink"
>;
const resolvedEffort = resolveReasoningEffortForProvider(PROVIDER, modelOptions?.effort);
const effort =
const baseEffort: Exclude<ClaudeCodeEffort, "ultrathink"> | null =
resolvedEffort && resolvedEffort !== "ultrathink" && options.includes(resolvedEffort)
? resolvedEffort
: options.includes(defaultReasoningEffort)
Expand All @@ -69,13 +75,15 @@ function getSelectedClaudeTraits(
? (modelOptions?.thinking ?? true)
: null;
const supportsFastMode = supportsClaudeFastMode(model);
const ultrathinkPromptControlled =
supportsClaudeUltrathinkKeyword(model) && isClaudeUltrathinkPrompt(prompt);
const effort: ClaudeCodeEffort | null = ultrathinkPromptControlled ? "ultrathink" : baseEffort;
return {
effort,
thinkingEnabled,
fastModeEnabled: supportsFastMode && modelOptions?.fastMode === true,
options,
ultrathinkPromptControlled:
supportsClaudeUltrathinkKeyword(model) && isClaudeUltrathinkPrompt(prompt),
ultrathinkPromptControlled,
supportsFastMode,
};
}
Expand Down
54 changes: 51 additions & 3 deletions apps/web/src/components/chat/CodexTraitsPicker.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import { COMPOSER_DRAFT_STORAGE_KEY, useComposerDraftStore } from "../../compose
async function mountPicker(props: {
reasoningEffort?: "low" | "medium" | "high" | "xhigh";
fastModeEnabled: boolean;
model?: string | null;
backendId?: string | null;
}) {
const threadId = ThreadId.makeUnsafe("thread-codex-traits");
const draftsByThreadId = {} as ReturnType<
typeof useComposerDraftStore.getState
>["draftsByThreadId"];
const model = props.model === undefined ? "gpt-5.4" : props.model;
draftsByThreadId[threadId] = {
prompt: "",
attachments: [],
Expand All @@ -25,7 +28,7 @@ async function mountPicker(props: {
promptEnhancement: null,
promptEnhancementOriginalPrompt: null,
provider: "codex",
model: null,
model,
modelOptions: {
codex: {
...(props.reasoningEffort ? { reasoningEffort: props.reasoningEffort } : {}),
Expand All @@ -44,7 +47,10 @@ async function mountPicker(props: {
});
const host = document.createElement("div");
document.body.append(host);
const screen = await render(<CodexTraitsPicker threadId={threadId} />, { container: host });
const screen = await render(
<CodexTraitsPicker threadId={threadId} model={model} backendId={props.backendId ?? null} />,
{ container: host },
);

return {
cleanup: async () => {
Expand Down Expand Up @@ -168,7 +174,10 @@ describe("CodexTraitsPicker", () => {

const host = document.createElement("div");
document.body.append(host);
const screen = await render(<CodexTraitsPicker threadId={threadId} />, { container: host });
const screen = await render(
<CodexTraitsPicker threadId={threadId} model="gpt-5.4" backendId={null} />,
{ container: host },
);

try {
await useComposerDraftStore.persist.rehydrate();
Expand All @@ -187,4 +196,43 @@ describe("CodexTraitsPicker", () => {
host.remove();
}
});

it("hides fast mode controls when the model does not support priority service tier", async () => {
const mounted = await mountPicker({
fastModeEnabled: false,
model: "gpt-5.3-codex",
});

try {
await page.getByRole("button").click();

await vi.waitFor(() => {
const text = document.body.textContent ?? "";
expect(text).toContain("Reasoning");
expect(text).not.toContain("Fast Mode");
});
} finally {
await mounted.cleanup();
}
});

it("hides fast mode controls when a non-OpenAI codex backend is selected", async () => {
const mounted = await mountPicker({
fastModeEnabled: false,
model: "gpt-5.4",
backendId: "ollama",
});

try {
await page.getByRole("button").click();

await vi.waitFor(() => {
const text = document.body.textContent ?? "";
expect(text).toContain("Reasoning");
expect(text).not.toContain("Fast Mode");
});
} finally {
await mounted.cleanup();
}
});
});
Loading
Loading