diff --git a/packages/adapters/codex-local/src/index.test.ts b/packages/adapters/codex-local/src/index.test.ts index 659d34c61f..c70529e6da 100644 --- a/packages/adapters/codex-local/src/index.test.ts +++ b/packages/adapters/codex-local/src/index.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + codexLocalReasoningEffortsForModel, DEFAULT_CODEX_LOCAL_MODEL, isCodexLocalFastModeSupported, models, @@ -7,23 +8,43 @@ import { } from "./index.js"; describe("codex local adapter metadata", () => { - it("advertises current GPT-5.6 Codex-capable OpenAI models by default", () => { + it("advertises current Codex-capable OpenAI models without changing the default", () => { const modelIds = models.map((model) => model.id); // Default to the concrete gpt-5.6-sol slug — Codex ships no metadata for the bare gpt-5.6 // alias, so it must not be advertised or used as the default (it triggers a fallback warning). expect(DEFAULT_CODEX_LOCAL_MODEL).toBe("gpt-5.6-sol"); - expect(modelIds.slice(0, 3)).toEqual([ + expect(modelIds.slice(0, 4)).toEqual([ "gpt-5.6-sol", + "gpt-6-astra", "gpt-5.6-terra", "gpt-5.6-luna", ]); expect(modelIds).not.toContain("gpt-5.6"); expect(isCodexLocalFastModeSupported(DEFAULT_CODEX_LOCAL_MODEL)).toBe(true); + expect(isCodexLocalFastModeSupported("gpt-6-astra")).toBe(true); expect(modelIds).not.toContain("gpt-5.3-codex"); expect(modelIds).not.toContain("gpt-5.3-codex-spark"); }); + it("uses the reasoning efforts advertised for GPT-6 Astra", () => { + expect(codexLocalReasoningEffortsForModel("gpt-6-astra")).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + "ultra", + ]); + expect(codexLocalReasoningEffortsForModel("gpt-5.6-sol")).toEqual([ + "minimal", + "low", + "medium", + "high", + "xhigh", + ]); + }); + it("normalizes the legacy bare gpt-5.6 alias to the concrete gpt-5.6-sol slug", () => { expect(normalizeCodexModel("gpt-5.6")).toBe("gpt-5.6-sol"); expect(normalizeCodexModel(" gpt-5.6 ")).toBe("gpt-5.6-sol"); diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index 008c1f0309..537c276654 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -13,6 +13,7 @@ export const SANDBOX_INSTALL_COMMAND = "npm install -g @openai/codex"; export const DEFAULT_CODEX_LOCAL_MODEL = PAPERCLIP_RUNNER_DEFAULT_MODELS.codex; export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true; export const CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS = [ + "gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", @@ -32,11 +33,40 @@ const CODEX_LOCAL_MODEL_ALIASES: Readonly> = { "gpt-5.6": "gpt-5.6-sol", }; +const CODEX_LOCAL_DEFAULT_REASONING_EFFORTS = [ + "minimal", + "low", + "medium", + "high", + "xhigh", +] as const; + +const CODEX_LOCAL_ASTRA_REASONING_EFFORTS = [ + "low", + "medium", + "high", + "xhigh", + "max", + "ultra", +] as const; + +export type CodexLocalReasoningEffort = + | (typeof CODEX_LOCAL_DEFAULT_REASONING_EFFORTS)[number] + | (typeof CODEX_LOCAL_ASTRA_REASONING_EFFORTS)[number]; + export function normalizeCodexModel(model: string | null | undefined): string { const normalizedModel = normalizeModelId(model); return CODEX_LOCAL_MODEL_ALIASES[normalizedModel] ?? normalizedModel; } +export function codexLocalReasoningEffortsForModel( + model: string | null | undefined, +): readonly CodexLocalReasoningEffort[] { + return normalizeCodexModel(model) === "gpt-6-astra" + ? CODEX_LOCAL_ASTRA_REASONING_EFFORTS + : CODEX_LOCAL_DEFAULT_REASONING_EFFORTS; +} + export function isCodexLocalKnownModel(model: string | null | undefined): boolean { const normalizedModel = normalizeModelId(model); if (!normalizedModel) return false; @@ -63,6 +93,7 @@ export function isCodexLocalFastModeSupported(model: string | null | undefined): export const models = [ // DEFAULT_CODEX_LOCAL_MODEL is gpt-5.6-sol, so it doubles as the first (default) 5.6 entry. { id: DEFAULT_CODEX_LOCAL_MODEL, label: DEFAULT_CODEX_LOCAL_MODEL }, + { id: "gpt-6-astra", label: "gpt-6-astra" }, { id: "gpt-5.6-terra", label: "gpt-5.6-terra" }, { id: "gpt-5.6-luna", label: "gpt-5.6-luna" }, { id: "gpt-5.4", label: "gpt-5.4" }, @@ -85,10 +116,10 @@ Core fields: - cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible) - instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to stdin prompt at runtime - model (string, optional): Codex model id -- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high|xhigh) passed via -c model_reasoning_effort=... +- modelReasoningEffort (string, optional): reasoning effort override passed via -c model_reasoning_effort=...; GPT-6 Astra supports low|medium|high|xhigh|max|ultra - promptTemplate (string, optional): run prompt template - search (boolean, optional): run codex with --search -- fastMode (boolean, optional): enable Codex Fast mode; supported on GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and passed through for manual model IDs +- fastMode (boolean, optional): enable Codex Fast mode; supported on GPT-6 Astra, GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and passed through for manual model IDs - dangerouslyBypassApprovalsAndSandbox (boolean, optional): run with bypass flag - command (string, optional): defaults to "codex" - extraArgs (string[], optional): additional CLI args @@ -119,7 +150,7 @@ Notes: - Paperclip injects desired local skills into the effective CODEX_HOME/skills/ directory at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. For new and updated agents, Paperclip assigns an isolated managed home at ~/.paperclip/instances//companies//agents//codex-home/skills/; when CODEX_HOME is explicitly overridden in adapter config, that override is used instead. - New and updated codex_local agents persist an empty OPENAI_API_KEY override by default so a host-level OPENAI_API_KEY cannot leak into Codex runs through process inheritance. Explicit CODEX_HOME overrides must not point at the shared company codex-home, $CODEX_HOME, or ~/.codex. - Some model/tool combinations reject certain effort levels (for example minimal with web search enabled). -- Fast mode is supported on GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`. +- Fast mode is supported on GPT-6 Astra, GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`. - When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling. - Codex ACP is the preferred auto lane when Node >=24.11.0 and the Codex ACP server are available. It reuses shared ACP prompt/runtime guidance, selected skill materialization into CODEX_HOME/skills, model/reasoning/fast-mode session config, and existing quota-window reporting. Auto selection falls back to CLI when ACP prerequisites are unavailable; explicit engine="acp" fails loudly. `; diff --git a/packages/adapters/codex-local/src/server/acp.test.ts b/packages/adapters/codex-local/src/server/acp.test.ts index d398e4e57d..b9f79d9d4f 100644 --- a/packages/adapters/codex-local/src/server/acp.test.ts +++ b/packages/adapters/codex-local/src/server/acp.test.ts @@ -489,6 +489,19 @@ describe("codex_local ACP lane", () => { }); }); + it("forwards GPT-6 Astra controls to the ACPX Codex target", () => { + expect(buildCodexAcpConfig({ + engine: "acp", + model: "gpt-6-astra", + modelReasoningEffort: "ultra", + fastMode: true, + })).toMatchObject({ + model: "gpt-6-astra", + modelReasoningEffort: "ultra", + fastMode: true, + }); + }); + it("normalizes the legacy bare gpt-5.6 alias to gpt-5.6-sol", () => { expect(buildCodexAcpConfig({ engine: "acp", model: "gpt-5.6" })).toMatchObject({ model: "gpt-5.6-sol", diff --git a/packages/adapters/codex-local/src/server/codex-args.test.ts b/packages/adapters/codex-local/src/server/codex-args.test.ts index 931dea19ba..3777f5e65e 100644 --- a/packages/adapters/codex-local/src/server/codex-args.test.ts +++ b/packages/adapters/codex-local/src/server/codex-args.test.ts @@ -2,6 +2,31 @@ import { describe, expect, it } from "vitest"; import { buildCodexExecArgs } from "./codex-args.js"; describe("buildCodexExecArgs", () => { + it("forwards GPT-6 Astra, its ultra reasoning effort, and fast mode", () => { + const result = buildCodexExecArgs({ + model: "gpt-6-astra", + modelReasoningEffort: "ultra", + fastMode: true, + }); + + expect(result.model).toBe("gpt-6-astra"); + expect(result.fastModeApplied).toBe(true); + expect(result.fastModeIgnoredReason).toBeNull(); + expect(result.args).toEqual([ + "exec", + "--json", + "--model", + "gpt-6-astra", + "-c", + 'model_reasoning_effort="ultra"', + "-c", + 'service_tier="fast"', + "-c", + "features.fast_mode=true", + "-", + ]); + }); + it("rewrites the legacy bare gpt-5.6 alias to gpt-5.6-sol and applies fast mode", () => { const result = buildCodexExecArgs({ model: "gpt-5.6", @@ -111,7 +136,7 @@ describe("buildCodexExecArgs", () => { expect(result.fastModeRequested).toBe(true); expect(result.fastModeApplied).toBe(false); expect(result.fastModeIgnoredReason).toContain( - "currently only supported on gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4 or manually configured model IDs", + "currently only supported on gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4 or manually configured model IDs", ); expect(result.args).toEqual([ "exec", diff --git a/packages/adapters/codex-local/src/ui/build-config.test.ts b/packages/adapters/codex-local/src/ui/build-config.test.ts index 760e276f15..c23e407148 100644 --- a/packages/adapters/codex-local/src/ui/build-config.test.ts +++ b/packages/adapters/codex-local/src/ui/build-config.test.ts @@ -63,6 +63,22 @@ describe("buildCodexLocalConfig", () => { }); }); + it("persists the exact GPT-6 Astra model and supported controls", () => { + const config = buildCodexLocalConfig( + makeValues({ + model: "gpt-6-astra", + thinkingEffort: "ultra", + fastMode: true, + }), + ); + + expect(config).toMatchObject({ + model: "gpt-6-astra", + modelReasoningEffort: "ultra", + fastMode: true, + }); + }); + it("omits model when the operator leaves it blank", () => { const config = buildCodexLocalConfig(makeValues({ model: "" })); diff --git a/ui/src/components/AgentConfigForm.render.test.tsx b/ui/src/components/AgentConfigForm.render.test.tsx index f9d0aaba71..fdc6c1697d 100644 --- a/ui/src/components/AgentConfigForm.render.test.tsx +++ b/ui/src/components/AgentConfigForm.render.test.tsx @@ -259,6 +259,7 @@ async function renderForm( const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); + const onSave = vi.fn(); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, @@ -274,7 +275,7 @@ async function renderForm( { expect(result.container.querySelector("select")).toBeNull(); }); + it("renders GPT-6 Astra and its model-specific reasoning efforts", async () => { + mockAgentsApi.adapterModels.mockResolvedValue([ + { id: "gpt-5.6-sol", label: "gpt-5.6-sol" }, + { id: "gpt-6-astra", label: "gpt-6-astra" }, + ]); + const result = await renderForm( + [makeEnvironment({ id: "local-1", name: "Local", driver: "local" })], + { + adapterConfig: { + model: "gpt-6-astra", + modelReasoningEffort: "ultra", + }, + }, + ); + roots.push(result.root); + + expect(result.container.textContent).toContain("gpt-6-astra"); + const effortButton = Array.from(result.container.querySelectorAll("button")) + .find((button) => button.textContent?.trim() === "Ultra"); + expect(effortButton).not.toBeUndefined(); + + await act(async () => { + effortButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const effortChoices = Array.from(document.body.querySelectorAll("button")) + .map((button) => button.textContent?.replace(/\s+/g, "").trim()); + expect(effortChoices).toContain("Maxmax"); + expect(effortChoices).toContain("Ultraultra"); + expect(effortChoices).not.toContain("Minimalminimal"); + }); + + it("removes a legacy incompatible effort when the model changes to Astra", async () => { + mockAgentsApi.adapterModels.mockResolvedValue([ + { id: "gpt-5.6-sol", label: "gpt-5.6-sol" }, + { id: "gpt-6-astra", label: "gpt-6-astra" }, + ]); + const result = await renderForm( + [makeEnvironment({ id: "local-1", name: "Local", driver: "local" })], + { + adapterConfig: { + model: "gpt-5.6-sol", + reasoningEffort: "minimal", + }, + }, + ); + roots.push(result.root); + + const modelButton = Array.from(result.container.querySelectorAll("button")) + .find((button) => button.textContent?.trim() === "gpt-5.6-sol"); + expect(modelButton).not.toBeUndefined(); + await act(async () => { + modelButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const astraOption = Array.from(document.body.querySelectorAll("button")) + .find((button) => button.textContent?.trim() === "gpt-6-astra"); + expect(astraOption).not.toBeUndefined(); + await act(async () => { + astraOption!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const saveButton = Array.from(result.container.querySelectorAll("button")) + .find((button) => button.textContent?.trim() === "Save"); + expect(saveButton).not.toBeUndefined(); + await act(async () => { + saveButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(result.onSave).toHaveBeenCalledWith({ + adapterConfig: { model: "gpt-6-astra" }, + replaceAdapterConfig: true, + }); + }); + it("keeps secret access out of the main Configuration content", async () => { const result = await renderForm([ makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 6af6a1e358..11a07b8f22 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -83,6 +83,7 @@ import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters"; import { buildAgentUpdatePatch, omitUndefinedEntries, type AgentConfigOverlay } from "../lib/agent-config-patch"; import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities"; import { resolveForcedKubernetesEnvironment } from "../lib/forced-kubernetes-environment"; +import { codexReasoningEffortOptions } from "../lib/codex-reasoning-effort"; /* ---- Create mode values ---- */ @@ -200,15 +201,6 @@ function formatArgList(value: unknown): string { return typeof value === "string" ? value : ""; } -const codexThinkingEffortOptions = [ - { id: "", label: "Auto" }, - { id: "minimal", label: "Minimal" }, - { id: "low", label: "Low" }, - { id: "medium", label: "Medium" }, - { id: "high", label: "High" }, - { id: "xhigh", label: "X-High" }, -] as const; - const openCodeThinkingEffortOptions = [ { id: "", label: "Auto" }, { id: "minimal", label: "Minimal" }, @@ -1094,7 +1086,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) { : "effort"; const thinkingEffortOptions = adapterType === "codex_local" - ? codexThinkingEffortOptions + ? codexReasoningEffortOptions(currentModelId, "Auto").map((option) => ({ + id: option.value, + label: option.label, + })) : adapterType === "cursor" ? cursorModeOptions : adapterType === "opencode_local" @@ -1564,11 +1559,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) { - isCreate - ? set!({ model: v }) - : mark("adapterConfig", "model", v || undefined) - } + onChange={(v) => { + const supportedEfforts = codexReasoningEffortOptions(v, "Auto"); + const clearUnsupportedEffort = adapterType === "codex_local" + && Boolean(currentThinkingEffort) + && !supportedEfforts.some((option) => option.value === currentThinkingEffort); + if (isCreate) { + set!({ + model: v, + ...(clearUnsupportedEffort ? { thinkingEffort: "" } : {}), + }); + return; + } + mark("adapterConfig", "model", v || undefined); + if (clearUnsupportedEffort) { + mark("adapterConfig", thinkingEffortKey, undefined); + mark("adapterConfig", "reasoningEffort", undefined); + } + }} open={modelOpen} onOpenChange={setModelOpen} allowDefault={adapterType !== "opencode_local"} diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 66e9327bb1..c012f1d19b 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -2151,7 +2151,7 @@ describe("IssueProperties", () => { }, ]); mockAgentsApi.adapterModels.mockResolvedValue([ - { id: "gpt-5.5", label: "GPT-5.5" }, + { id: "gpt-6-astra", label: "gpt-6-astra" }, { id: "gpt-5.4", label: "GPT-5.4" }, ]); @@ -2178,7 +2178,7 @@ describe("IssueProperties", () => { let modelButton: HTMLButtonElement | undefined; await waitForAssertion(() => { modelButton = Array.from(container.querySelectorAll("button")) - .find((button) => button.textContent?.includes("GPT-5.5")); + .find((button) => button.textContent?.includes("gpt-6-astra")); expect(modelButton).not.toBeUndefined(); }); @@ -2189,7 +2189,7 @@ describe("IssueProperties", () => { expect(onUpdate).toHaveBeenCalledWith({ assigneeAdapterOverrides: { adapterConfig: { - model: "gpt-5.5", + model: "gpt-6-astra", modelReasoningEffort: "high", }, }, @@ -2198,6 +2198,67 @@ describe("IssueProperties", () => { act(() => root.unmount()); }); + it("keeps Astra-only task efforts when the task inherits the agent model", async () => { + const onUpdate = vi.fn(); + mockAgentsApi.list.mockResolvedValue([ + { + id: "agent-1", + name: "Senior Product Engineer", + role: "engineer", + title: null, + status: "active", + adapterType: "codex_local", + adapterConfig: { model: "gpt-6-astra" }, + icon: null, + }, + ]); + mockAgentsApi.adapterModels.mockResolvedValue([ + { id: "gpt-6-astra", label: "gpt-6-astra" }, + { id: "gpt-5.4", label: "GPT-5.4" }, + ]); + + const root = renderProperties(container, { + issue: createIssue({ + assigneeAgentId: "agent-1", + assigneeAdapterOverrides: { + adapterConfig: { modelReasoningEffort: "ultra" }, + }, + }), + childIssues: [], + onUpdate, + }); + await flush(); + await flush(); + + expect(container.textContent).toContain("Ultra"); + expect(container.textContent).toContain("Max"); + expect(container.textContent).not.toContain("Minimal"); + + const defaultModelButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.includes("Default model")); + expect(defaultModelButton).not.toBeUndefined(); + await act(async () => { + defaultModelButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + const defaultModelOption = Array.from(document.body.querySelectorAll("button")) + .filter((button) => button.textContent?.trim() === "Default model") + .at(-1); + expect(defaultModelOption).not.toBeUndefined(); + await act(async () => { + defaultModelOption!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(onUpdate).toHaveBeenCalledWith({ + assigneeAdapterOverrides: { + adapterConfig: { modelReasoningEffort: "ultra" }, + }, + }); + + act(() => root.unmount()); + }); + it("clears existing assignee adapter overrides from the properties pane", async () => { const onUpdate = vi.fn(); mockAgentsApi.list.mockResolvedValue([ diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index 4e0004a753..fcedbb7376 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -546,6 +546,49 @@ describe("NewIssueDialog", () => { act(() => root.unmount()); }); + it("shows Astra-only efforts when a task inherits the agent model", async () => { + dialogState.newIssueDefaults = { + title: "Use inherited Astra", + assigneeAgentId: "agent-1", + }; + mockAgentsApi.list.mockResolvedValue([ + { + id: "agent-1", + name: "CodexCoder", + status: "active", + adapterType: "codex_local", + adapterConfig: { model: "gpt-6-astra" }, + runtimeConfig: {}, + permissions: {}, + }, + ]); + + const { root } = renderDialog(container); + await waitForAssertion(() => { + expect(container.textContent).toContain("Codex options"); + }); + + const codexOptionsButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.includes("Codex options")); + expect(codexOptionsButton).not.toBeUndefined(); + await act(async () => { + codexOptionsButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + const customLane = Array.from(container.querySelectorAll('button[role="radio"]')) + .find((button) => button.textContent?.trim() === "Custom"); + expect(customLane).not.toBeUndefined(); + await act(async () => { + customLane!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(container.textContent).toContain("Ultra"); + expect(container.textContent).toContain("Max"); + expect(container.textContent).not.toContain("Minimal"); + + act(() => root.unmount()); + }); + it("warns when the selected assignee is a paused imported agent", async () => { dialogState.newIssueDefaults = { title: "Compare onboarding flows", diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 34bd5c5407..216bd2e820 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -81,6 +81,7 @@ import { InlineBanner } from "./InlineBanner"; import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector"; import { getTrustPreset } from "../lib/trust-policy-ui"; import { ReusableExecutionWorkspaceSelect } from "./ReusableExecutionWorkspaceSelect"; +import { codexReasoningEffortOptions } from "../lib/codex-reasoning-effort"; const DRAFT_KEY = "paperclip:issue-draft"; const DEBOUNCE_MS = 800; @@ -184,14 +185,6 @@ const ISSUE_THINKING_EFFORT_OPTIONS = { { value: "medium", label: "Medium" }, { value: "high", label: "High" }, ], - codex_local: [ - { value: "", label: "Default" }, - { value: "minimal", label: "Minimal" }, - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High" }, - { value: "xhigh", label: "X-High" }, - ], opencode_local: [ { value: "", label: "Default" }, { value: "minimal", label: "Minimal" }, @@ -594,6 +587,11 @@ export function NewIssueDialog() { [agents, selectedAssigneeAgentId], ); const assigneeAdapterType = selectedAssigneeAgent?.adapterType ?? null; + const assigneePrimaryModel = isRecord(selectedAssigneeAgent?.adapterConfig) + && typeof selectedAssigneeAgent.adapterConfig.model === "string" + ? selectedAssigneeAgent.adapterConfig.model + : ""; + const effectiveAssigneeModel = assigneeModelOverride || assigneePrimaryModel; const supportsAssigneeOverrides = Boolean( assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType), ); @@ -938,7 +936,7 @@ export function NewIssueDialog() { } const validThinkingValues = assigneeAdapterType === "codex_local" - ? ISSUE_THINKING_EFFORT_OPTIONS.codex_local + ? codexReasoningEffortOptions(effectiveAssigneeModel) : assigneeAdapterType === "opencode_local" ? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local : ISSUE_THINKING_EFFORT_OPTIONS.claude_local; @@ -948,6 +946,7 @@ export function NewIssueDialog() { }, [ supportsAssigneeOverrides, assigneeAdapterType, + effectiveAssigneeModel, assigneeThinkingEffort, ]); @@ -1194,7 +1193,7 @@ export function NewIssueDialog() { : "Agent options"; const thinkingEffortOptions = assigneeAdapterType === "codex_local" - ? ISSUE_THINKING_EFFORT_OPTIONS.codex_local + ? codexReasoningEffortOptions(effectiveAssigneeModel) : assigneeAdapterType === "opencode_local" ? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local : ISSUE_THINKING_EFFORT_OPTIONS.claude_local; diff --git a/ui/src/components/agent-config-primitives.tsx b/ui/src/components/agent-config-primitives.tsx index e209036db1..673fe45b2c 100644 --- a/ui/src/components/agent-config-primitives.tsx +++ b/ui/src/components/agent-config-primitives.tsx @@ -34,7 +34,7 @@ export const help: Record = { dangerouslySkipPermissions: "Run unattended by auto-approving adapter permission prompts when supported.", dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.", search: "Enable Codex web search capability during runs.", - fastMode: "Enable Codex Fast mode. This burns credits/tokens much faster and is supported on GPT-5.6, GPT-5.5, GPT-5.4, and manual Codex model IDs.", + fastMode: "Enable Codex Fast mode. This burns credits/tokens much faster and is supported on GPT-6 Astra, GPT-5.6, GPT-5.5, GPT-5.4, and manual Codex model IDs.", workspaceStrategy: "How Paperclip should realize an execution workspace for this agent. Keep project_primary for normal cwd execution, or use git_worktree for issue-scoped isolated checkouts.", workspaceBaseRef: "Base git ref used when creating a worktree branch. Leave blank to use the resolved workspace ref or HEAD.", workspaceBranchTemplate: "Template for naming derived branches. Supports {{issue.identifier}}, {{issue.title}}, {{agent.name}}, {{project.id}}, {{workspace.repoRef}}, and {{slug}}.", diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx index d1af86d380..6e43d07208 100644 --- a/ui/src/components/issue-properties/IssueProperties.tsx +++ b/ui/src/components/issue-properties/IssueProperties.tsx @@ -730,6 +730,10 @@ export function IssueProperties({ const assigneeOverrideAdapterConfig = asRecord(assigneeAdapterOverrides?.adapterConfig); const assigneeOverrideModel = typeof assigneeOverrideAdapterConfig.model === "string" ? assigneeOverrideAdapterConfig.model : ""; + const assigneePrimaryAdapterConfig = asRecord(assignee?.adapterConfig); + const assigneePrimaryModel = + typeof assigneePrimaryAdapterConfig.model === "string" ? assigneePrimaryAdapterConfig.model : ""; + const effectiveAssigneeModel = assigneeOverrideModel || assigneePrimaryModel; const assigneeOverrideThinkingEffort = thinkingEffortValueFor( assigneeAdapterType, assigneeOverrideAdapterConfig, @@ -790,6 +794,24 @@ export function IssueProperties({ } updateAssigneeAdapterOverrides(buildAssigneeOverrideWithConfig(nextConfig)); }; + const updateAssigneeOverrideModel = (nextModel: string) => { + const nextConfig: Record = { + ...assigneeOverrideAdapterConfig, + model: nextModel || undefined, + }; + if ( + assigneeAdapterType === "codex_local" + && assigneeOverrideThinkingEffort + && !thinkingEffortOptionsFor(assigneeAdapterType, nextModel || assigneePrimaryModel).some( + (option) => option.value === assigneeOverrideThinkingEffort, + ) + ) { + delete nextConfig.modelReasoningEffort; + delete nextConfig.reasoningEffort; + delete nextConfig.effort; + } + updateAssigneeAdapterOverrides(buildAssigneeOverrideWithConfig(nextConfig)); + }; const setAssigneeOverrideLane = (lane: IssueModelLane) => { if (lane === "primary") { updateAssigneeAdapterOverrides(null); @@ -855,13 +877,13 @@ export function IssueProperties({ noneLabel="Default model" searchPlaceholder="Search models..." emptyMessage="No models found." - onChange={(model) => updateAssigneeOverrideConfig({ model: model || undefined })} + onChange={updateAssigneeOverrideModel} />
Thinking effort
- {thinkingEffortOptionsFor(assigneeAdapterType).map((option) => ( + {thinkingEffortOptionsFor(assigneeAdapterType, effectiveAssigneeModel).map((option) => (