mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
## 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>
125 lines
5.0 KiB
TypeScript
125 lines
5.0 KiB
TypeScript
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 }>;
|
|
executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null;
|
|
} | null | undefined) {
|
|
if (!project) return null;
|
|
return project.executionWorkspacePolicy?.defaultProjectWorkspaceId
|
|
?? project.workspaces?.find((workspace) => workspace.isPrimary)?.id
|
|
?? project.workspaces?.[0]?.id
|
|
?? null;
|
|
}
|
|
|
|
export function defaultExecutionWorkspaceModeForProject(project: { executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null } | null } | null | undefined) {
|
|
const defaultMode = project?.executionWorkspacePolicy?.enabled ? project.executionWorkspacePolicy.defaultMode : null;
|
|
if (defaultMode === "isolated_workspace" || defaultMode === "operator_branch") return defaultMode;
|
|
if (defaultMode === "adapter_default") return "agent_default";
|
|
return "shared_workspace";
|
|
}
|
|
|
|
function primaryWorkspaceIdForProject(project: Pick<Project, "primaryWorkspace" | "workspaces"> | null | undefined) {
|
|
return project?.primaryWorkspace?.id
|
|
?? project?.workspaces.find((workspace) => workspace.isPrimary)?.id
|
|
?? project?.workspaces[0]?.id
|
|
?? null;
|
|
}
|
|
|
|
export function isMainIssueWorkspace(input: {
|
|
issue: Pick<Issue, "projectWorkspaceId" | "currentExecutionWorkspace">;
|
|
project: Pick<Project, "primaryWorkspace" | "workspaces"> | null | undefined;
|
|
}) {
|
|
const workspace = input.issue.currentExecutionWorkspace ?? null;
|
|
const primaryWorkspaceId = primaryWorkspaceIdForProject(input.project);
|
|
const linkedProjectWorkspaceId = workspace?.projectWorkspaceId ?? input.issue.projectWorkspaceId ?? null;
|
|
if (workspace) {
|
|
if (workspace.mode !== "shared_workspace") return false;
|
|
if (!linkedProjectWorkspaceId || !primaryWorkspaceId) return true;
|
|
return workspace.mode === "shared_workspace" && linkedProjectWorkspaceId === primaryWorkspaceId;
|
|
}
|
|
if (!linkedProjectWorkspaceId || !primaryWorkspaceId) return true;
|
|
return linkedProjectWorkspaceId === primaryWorkspaceId;
|
|
}
|
|
|
|
export function toDateTimeLocalValue(value: string | null | undefined) {
|
|
if (!value) return "";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
const offsetMs = date.getTimezoneOffset() * 60_000;
|
|
return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16);
|
|
}
|
|
|
|
export const ISSUE_THINKING_EFFORT_OPTIONS = {
|
|
claude_local: [
|
|
{ value: "", label: "Default" },
|
|
{ value: "low", label: "Low" },
|
|
{ value: "medium", label: "Medium" },
|
|
{ value: "high", label: "High" },
|
|
],
|
|
opencode_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" },
|
|
{ value: "max", label: "Max" },
|
|
],
|
|
} as const;
|
|
|
|
export function asRecord(value: unknown): Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: {};
|
|
}
|
|
|
|
export function compactRecord(record: Record<string, unknown>) {
|
|
return Object.fromEntries(
|
|
Object.entries(record).filter(([, value]) => value !== undefined),
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export function thinkingEffortKeyFor(adapterType: string | null | undefined) {
|
|
if (adapterType === "codex_local") return "modelReasoningEffort";
|
|
if (adapterType === "opencode_local") return "variant";
|
|
return "effort";
|
|
}
|
|
|
|
export function thinkingEffortValueFor(adapterType: string | null | undefined, adapterConfig: Record<string, unknown>) {
|
|
if (adapterType === "codex_local") {
|
|
return String(adapterConfig.modelReasoningEffort ?? adapterConfig.reasoningEffort ?? adapterConfig.effort ?? "");
|
|
}
|
|
if (adapterType === "opencode_local") {
|
|
return String(adapterConfig.variant ?? "");
|
|
}
|
|
return String(adapterConfig.effort ?? "");
|
|
}
|
|
|
|
export function overrideLane(overrides: Issue["assigneeAdapterOverrides"]): IssueModelLane {
|
|
if (overrides?.adapterConfig) return "custom";
|
|
return "primary";
|
|
}
|
|
|
|
export function sortAdapterModels(models: AdapterModel[]) {
|
|
return [...models].sort((a, b) => {
|
|
const providerA = extractProviderIdWithFallback(a.id);
|
|
const providerB = extractProviderIdWithFallback(b.id);
|
|
const byProvider = providerA.localeCompare(providerB);
|
|
if (byProvider !== 0) return byProvider;
|
|
return a.id.localeCompare(b.id);
|
|
});
|
|
}
|