mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
feat(codex): add GPT-6 Astra support (#12851)
## Thinking Path > - Paperclip is the open source app that people use to manage AI agents for work. > - The Codex local adapter supplies model metadata to the server and the user interface. > - OpenAI now lists `gpt-6-astra` as a supported Codex model. > - Paperclip did not list this model or its model-specific controls. > - This pull request adds the model through the existing adapter metadata path. > - The benefit is that agents and task overrides can use the exact model ID and supported controls. ## Linked Issues or Issue Description **Subsystem affected** `packages/adapters` and `ui` **Problem or motivation** Paperclip does not expose `gpt-6-astra` in Codex model selectors. Operators cannot select and save the model through the normal agent and task forms. **Proposed solution** Register the exact model ID in the Codex local adapter. Use the adapter as the source for the model-specific reasoning options. Preserve the current default model. Forward the saved model, reasoning effort, and fast-mode controls through both Codex execution lanes. **Alternatives considered** A user-interface-only model list would duplicate adapter metadata. A model alias would not match the official model ID. Both options were rejected. **Roadmap alignment** This is a small adapter compatibility update. It does not duplicate a planned item in `ROADMAP.md`. ## What Changed - Added `gpt-6-astra` to the Codex local adapter model registry and fast-mode support list. - Added the official Astra reasoning efforts: `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`. - Used the adapter metadata in agent and task model selectors. - Preserved supported effort choices when the model changes. Cleared an effort only when the new model does not support it. - Added tests for registration, user-interface selection, configuration persistence, and CLI and ACP forwarding. ## Verification - `pnpm exec vitest run packages/adapters/codex-local/src/index.test.ts packages/adapters/codex-local/src/server/acp.test.ts packages/adapters/codex-local/src/server/codex-args.test.ts packages/adapters/codex-local/src/ui/build-config.test.ts ui/src/lib/codex-reasoning-effort.test.ts ui/src/components/AgentConfigForm.render.test.tsx ui/src/components/IssueProperties.test.tsx ui/src/components/NewIssueDialog.test.tsx ui/src/lib/issue-assignee-overrides.test.ts` passed 245 tests. - `pnpm -r typecheck` passed. - `pnpm check:token-gates` passed all four gates across 939 files. - `pnpm --filter @paperclipai/ui build` passed and supplied isolated user-interface build proof. - `pnpm build` passed. - `pnpm test:run` passed 5,812 tests and failed 24 workspace-runtime tests in this isolated host. The failures use invalid generated ports above 65,535, incomplete nested-worktree fixture configuration, or `/tmp` path aliases. The focused tests for this change all pass. GitHub CI must pass before review handoff. - GitHub CI run `33918372718` passed all required checks and the aggregate verify gate on exact head `6ac6be2cee0a5996c82bdf674fcb7f46cb4c5fde`. - Independent engineering review approved the exact remediation head after 170/170 reviewer tests passed. - Greptile reported 5/5 with no open review threads on exact head `6ac6be2cee0a5996c82bdf674fcb7f46cb4c5fde`. - The model ID and capabilities were checked against the [official OpenAI Codex model list](https://developers.openai.com/codex/models). ## Risks - Low risk. The change adds one model and model-specific selector options. It does not change the default model. - OpenAI can change model capabilities later. The adapter metadata must stay aligned with the official Codex metadata. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex with model ID `gpt-5.6-sol`, a 272,000-token context window, reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d593463ab6
commit
77312ee2d9
16 files changed
+436
-49
No files matched your search
@@ -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(
|
||||
<AgentConfigForm
|
||||
mode="edit"
|
||||
agent={makeAgent(agentOverrides)}
|
||||
onSave={vi.fn()}
|
||||
onSave={onSave}
|
||||
hidePromptTemplate
|
||||
content={options.content}
|
||||
showAdapterTypeField={false}
|
||||
@@ -287,7 +288,7 @@ async function renderForm(
|
||||
});
|
||||
|
||||
await flushReact();
|
||||
return { container, root };
|
||||
return { container, root, onSave };
|
||||
}
|
||||
|
||||
async function renderCreateForm(
|
||||
@@ -724,6 +725,84 @@ describe("AgentConfigForm environment selector", () => {
|
||||
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" }),
|
||||
|
||||
@@ -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) {
|
||||
<ModelDropdown
|
||||
models={models}
|
||||
value={currentModelId}
|
||||
onChange={(v) =>
|
||||
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"}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const help: Record<string, string> = {
|
||||
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}}.",
|
||||
|
||||
@@ -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<string, unknown> = {
|
||||
...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}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs text-muted-foreground">Thinking effort</div>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{thinkingEffortOptionsFor(assigneeAdapterType).map((option) => (
|
||||
{thinkingEffortOptionsFor(assigneeAdapterType, effectiveAssigneeModel).map((option) => (
|
||||
<button
|
||||
key={option.value || "default"}
|
||||
className={cn(
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AdapterModel } from "../../api/agents";
|
||||
import type { Issue, Project } from "@paperclipai/shared";
|
||||
import { extractProviderIdWithFallback } from "../../lib/model-utils";
|
||||
import type { IssueModelLane } from "../../lib/issue-assignee-overrides";
|
||||
import { codexReasoningEffortOptions } from "../../lib/codex-reasoning-effort";
|
||||
|
||||
export function defaultProjectWorkspaceIdForProject(project: {
|
||||
workspaces?: Array<{ id: string; isPrimary: boolean }>;
|
||||
@@ -59,14 +60,6 @@ export 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" },
|
||||
@@ -90,8 +83,11 @@ export function compactRecord(record: Record<string, unknown>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function thinkingEffortOptionsFor(adapterType: string | null | undefined) {
|
||||
if (adapterType === "codex_local") return ISSUE_THINKING_EFFORT_OPTIONS.codex_local;
|
||||
export function thinkingEffortOptionsFor(
|
||||
adapterType: string | null | undefined,
|
||||
model?: string | null,
|
||||
) {
|
||||
if (adapterType === "codex_local") return codexReasoningEffortOptions(model);
|
||||
if (adapterType === "opencode_local") return ISSUE_THINKING_EFFORT_OPTIONS.opencode_local;
|
||||
return ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user