mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
fix(runner): honor Codex effort selected in composer (#14568)
## Thinking Path > - Paperclip manages AI agents that work on assigned tasks. > - The task composer lets a person choose an assignee, model, and effort for the next run. > - A Paperclip Runner agent can use Codex as its provider. > - The composer hid Codex effort for that agent because it checked only the older Codex adapter. > - The native Runner input also did not carry an effort choice to Codex. > - This pull request carries the chosen effort from the composer to each Codex turn. > - People can now select a supported effort and get the effort they selected. ## Linked Issues or Issue Description Refs #14322 **What happened?** The composer showed a model but no effort slider when the assignee used Paperclip Runner with the Codex provider. A task-level model override also did not reach the native Runner input. **Expected behavior** The composer shows effort choices for a known Codex model. The next native Codex turn uses the selected model and effort. **Steps to reproduce** 1. Open a task composer. 2. Select an agent that uses Paperclip Runner with the Codex provider. 3. Select a known Codex model such as `gpt-6-astra`. 4. Open the assignee and model picker. The effort slider is missing before this change. ## What Changed - Show known Codex effort levels for Paperclip Runner Codex assignees. - Save the task effort override in the native run input and send it to Codex on each turn. - Apply the task's merged model and effort overrides when the native run starts. - Apply a task model override for OpenCode Runner without changing the agent's provider. - Add Runner effort tests and desktop and mobile Storybook cases. ## Verification - `pnpm -r typecheck` passed. - `pnpm build` passed. - `pnpm build-storybook` passed. - `pnpm check:token-gates` passed. - Focused UI, server, Runner contract, and Codex driver tests passed. - The full CI test matrix, build, typecheck, and canary dry run passed on the latest head. ## Risks - Native Runner inputs add an optional Codex effort field to the current v5 input. Older inputs keep their previous behavior. - A known model rejects an effort that its catalog does not support. Unknown models do not show a slider. > This fixes an existing composer bug. I checked `ROADMAP.md`; it does not describe this bug as planned work. ## Model Used OpenAI Codex, GPT-6. The exact deployment ID and context window are not exposed in this session. The model used reasoning, code execution, and repository tools. ## 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> Co-authored-by: OpenAI GPT-6 Astra <noreply@openai.com>
This commit is contained in:
19 files changed
+185
-9
No files matched your search
@@ -115,6 +115,8 @@ pending card.
|
||||
Use **Composer → Model and effort picker** to review harness-specific model
|
||||
choices. Codex uses the curated adapter catalog unless the instance declares
|
||||
`PAPERCLIP_ADAPTER_MODELS`; general OpenAI API models are not Codex choices.
|
||||
The Paperclip Runner Codex profile shows the same known model effort levels.
|
||||
Its selected effort is saved with the run and sent to Codex for each turn.
|
||||
|
||||
The Storybook visual regression suite uses external PNG baselines instead of
|
||||
committed screenshots:
|
||||
|
||||
@@ -145,6 +145,9 @@ function createTransportBackedNativeSessionBackend(
|
||||
return new HarnessDriverBackend(
|
||||
new CodexAppServerDriver({
|
||||
...(input.provider.model ? { model: input.provider.model } : {}),
|
||||
...(input.provider.kind === "codex" && "reasoningEffort" in input.provider && input.provider.reasoningEffort
|
||||
? { reasoningEffort: input.provider.reasoningEffort }
|
||||
: {}),
|
||||
// Runnerd owns provider permissions for non-Codex facades. Their
|
||||
// Codex-compatible surface must never open a second approval channel.
|
||||
approvalPolicy:
|
||||
|
||||
@@ -120,6 +120,21 @@ describe("NativeExecutionInputV1", () => {
|
||||
schema: "paperclip.native-execution-input.v4",
|
||||
provider: { kind: "codex", approvalPolicy: "on-request" },
|
||||
});
|
||||
const withEffort = parseNativeExecutionInput({
|
||||
...current,
|
||||
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
|
||||
provider: { kind: "codex", model: "gpt-6-astra", approvalPolicy: "on-request", reasoningEffort: "ultra" },
|
||||
});
|
||||
expect(withEffort.provider).toMatchObject({ kind: "codex", reasoningEffort: "ultra" });
|
||||
expect(parseNativeExecutionInput(withEffort)).toEqual(withEffort);
|
||||
expect(() => parseNativeExecutionInput({
|
||||
...withEffort,
|
||||
provider: { kind: "codex", model: "gpt-6-astra", approvalPolicy: "on-request", reasoningEffort: "impossible" },
|
||||
})).toThrow("reasoningEffort");
|
||||
expect(() => parseNativeExecutionInput({
|
||||
...current,
|
||||
provider: { kind: "codex", model: null, approvalPolicy: "on-request", reasoningEffort: "ultra" },
|
||||
})).toThrow("input.provider");
|
||||
expect(() => parseNativeExecutionInput({
|
||||
...parsed,
|
||||
schema: "paperclip.native-execution-input.v4",
|
||||
|
||||
@@ -140,6 +140,10 @@ export type NativeProviderConfigV4 =
|
||||
profile: NativeAcpxProfileSnapshot;
|
||||
};
|
||||
|
||||
export type NativeProviderConfigV5 =
|
||||
| Exclude<NativeProviderConfigV4, { kind: "codex" }>
|
||||
| (Extract<NativeProviderConfigV4, { kind: "codex" }> & { reasoningEffort?: string });
|
||||
|
||||
export interface NativeExecutionInputV1 {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V1;
|
||||
binding: {
|
||||
@@ -217,8 +221,9 @@ export interface NativeCompletionSources {
|
||||
criteria: Array<{ id: string; source: NativeCompletionSource }>;
|
||||
}
|
||||
|
||||
export interface NativeExecutionInputV5 extends Omit<NativeExecutionInputV4, "schema"> {
|
||||
export interface NativeExecutionInputV5 extends Omit<NativeExecutionInputV4, "schema" | "provider"> {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA;
|
||||
provider: NativeProviderConfigV5;
|
||||
completionSources?: NativeCompletionSources;
|
||||
}
|
||||
|
||||
@@ -447,7 +452,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
: provider.kind === "acpx"
|
||||
? ["kind", "agent", "model", isV4 ? "permissionMode" : "permissionPolicy", "profile"]
|
||||
: provider.kind === "codex" && isV4
|
||||
? ["kind", "model", "approvalPolicy"]
|
||||
? ["kind", "model", "approvalPolicy", ...(isV5 ? ["reasoningEffort"] : [])]
|
||||
: provider.kind === "opencode" && isV4
|
||||
? ["kind", "model", "permissionMode"]
|
||||
: ["kind", "model"],
|
||||
@@ -468,7 +473,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
if (provider.kind === "opencode" && (providerModel === null || !providerModel.includes("/"))) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for opencode in provider/model form");
|
||||
}
|
||||
let parsedProvider: NativeProviderConfig;
|
||||
let parsedProvider: NativeProviderConfig | NativeProviderConfigV5;
|
||||
if (provider.kind === "claude_managed") {
|
||||
if (providerModel === null) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for claude_managed");
|
||||
@@ -640,12 +645,18 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
if (isV4 && provider.approvalPolicy !== "never" && provider.approvalPolicy !== "on-request" && provider.approvalPolicy !== "untrusted") {
|
||||
throw new NativeExecutionInputError("input.provider.approvalPolicy must be never, on-request, or untrusted");
|
||||
}
|
||||
if (isV5 && provider.reasoningEffort !== undefined && !["minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(String(provider.reasoningEffort))) {
|
||||
throw new NativeExecutionInputError("input.provider.reasoningEffort is unsupported");
|
||||
}
|
||||
parsedProvider = {
|
||||
kind: "codex",
|
||||
model: providerModel,
|
||||
...(isV4
|
||||
? { approvalPolicy: provider.approvalPolicy as NativeCodexApprovalPolicy }
|
||||
: {}),
|
||||
...(isV5 && provider.reasoningEffort !== undefined
|
||||
? { reasoningEffort: text(provider.reasoningEffort, "input.provider.reasoningEffort") }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
if (!Array.isArray(contract.criteria) || contract.criteria.length === 0) {
|
||||
@@ -761,6 +772,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
return {
|
||||
...withPermissions,
|
||||
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
|
||||
provider: parsedProvider as NativeProviderConfigV5,
|
||||
...(input.completionSources !== undefined ? { completionSources: parseCompletionSources(input.completionSources) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -930,6 +930,7 @@ export class CodexAppServerDriver implements HarnessDriver {
|
||||
goalCapability: this.#goalCapability,
|
||||
dynamicTools: this.#providerDynamicTools(),
|
||||
skillInputs: this.#options.skillInputs,
|
||||
reasoningEffort: this.#options.reasoningEffort,
|
||||
dynamicToolHandler: this.#options.dynamicToolHandler,
|
||||
completionFeedback: this.#options.completionFeedback,
|
||||
});
|
||||
|
||||
@@ -1436,6 +1436,18 @@ describe("Codex app-server Codex driver", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends the selected reasoning effort with the Codex turn", async () => {
|
||||
const transport = new FakeCodexTransport();
|
||||
const session = await makeDriver([transport], { model: "gpt-6-astra", reasoningEffort: "ultra" }).openSession({
|
||||
runId: "run-effort",
|
||||
normalizedSessionId: "session-effort",
|
||||
workingDirectory: TEST_WORKING_DIRECTORY,
|
||||
});
|
||||
await session.startTurn({ message: { role: "user", text: "Continue" } });
|
||||
expect(transport.calls.find((call) => call.method === "turn/start")?.params).toMatchObject({ effort: "ultra" });
|
||||
await session.close({ reason: "test complete" });
|
||||
});
|
||||
|
||||
it("fails closed when the installed app-server does not confirm plan mode", async () => {
|
||||
const transport = new FakeCodexTransport();
|
||||
transport.confirmCollaborationMode = false;
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface CodexAppServerDriverOptions {
|
||||
taskEnvelope: CodexTaskEnvelope;
|
||||
/** Explicit provider model selected by the persisted native execution. */
|
||||
model?: string;
|
||||
/** Per-run reasoning effort sent with each Codex turn, including resumed turns. */
|
||||
reasoningEffort?: string;
|
||||
approvalPolicy?: "never" | "on-request" | "untrusted";
|
||||
baseInstructions?: string;
|
||||
includeSkillInstructions?: boolean;
|
||||
|
||||
@@ -221,6 +221,7 @@ export class CodexHarnessSession
|
||||
try {
|
||||
response = await this.transport.request("turn/start", {
|
||||
threadId: this.opened.threadId,
|
||||
...(this.reasoningEffort ? { effort: this.reasoningEffort } : {}),
|
||||
cwd: this.opened.context.workingDirectory,
|
||||
permissions:
|
||||
text(record(record(this.opened.context.sandbox).permissionProfile).id) || (requestedMode === "plan"
|
||||
|
||||
@@ -101,6 +101,7 @@ export class CodexSessionState {
|
||||
readonly goalReasonCode: string | null;
|
||||
readonly goalReason: string | null;
|
||||
readonly skillInputs: NonNullable<CodexAppServerDriverOptions["skillInputs"]>;
|
||||
readonly reasoningEffort: string | undefined;
|
||||
readonly dynamicTools: readonly Readonly<Record<string, unknown>>[];
|
||||
readonly completionFeedback: CodexAppServerDriverOptions["completionFeedback"];
|
||||
readonly dynamicToolHandler: CodexAppServerDriverOptions["dynamicToolHandler"];
|
||||
@@ -174,6 +175,7 @@ export class CodexSessionState {
|
||||
goalReasonCode: string | null;
|
||||
goalReason: string | null;
|
||||
skillInputs?: CodexAppServerDriverOptions["skillInputs"];
|
||||
reasoningEffort?: string;
|
||||
dynamicTools: readonly Readonly<Record<string, unknown>>[];
|
||||
completionFeedback?: CodexAppServerDriverOptions["completionFeedback"];
|
||||
dynamicToolHandler?: CodexAppServerDriverOptions["dynamicToolHandler"];
|
||||
@@ -198,6 +200,7 @@ export class CodexSessionState {
|
||||
this.goalReasonCode = input.goalReasonCode;
|
||||
this.goalReason = input.goalReason;
|
||||
this.skillInputs = structuredClone(input.skillInputs ?? []);
|
||||
this.reasoningEffort = input.reasoningEffort;
|
||||
this.dynamicTools = input.dynamicTools;
|
||||
this.dynamicToolHandler = input.dynamicToolHandler;
|
||||
this.completionFeedback = input.completionFeedback;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertAgentCoreProfileRecoveryBinding,
|
||||
assertManagedProfileRecoveryBinding,
|
||||
projectPaperclipRunnerTaskConfig,
|
||||
resolvePaperclipRunnerNativeProviderInput,
|
||||
} from "./native-runtime/provider-profile.js";
|
||||
|
||||
@@ -44,6 +45,45 @@ describe("Paperclip Runner native provider configuration", () => {
|
||||
).toThrow("codexPermissionMode set to never");
|
||||
});
|
||||
|
||||
it("projects Codex effort only when the selected model supports it", () => {
|
||||
expect(resolvePaperclipRunnerNativeProviderInput({
|
||||
backend: "codex_app_server",
|
||||
adapterConfig: { provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "ultra" },
|
||||
})).toMatchObject({ provider: "codex", model: "gpt-6-astra", codexReasoningEffort: "ultra" });
|
||||
expect(() => resolvePaperclipRunnerNativeProviderInput({
|
||||
backend: "codex_app_server",
|
||||
adapterConfig: { provider: "codex", model: "gpt-6-luna", modelReasoningEffort: "ultra" },
|
||||
})).toThrow("reasoning effort is not supported");
|
||||
});
|
||||
|
||||
it("uses only task model and effort overrides without changing the Runner provider", () => {
|
||||
const base = { provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "ultra", codexPermissionMode: "never" };
|
||||
const projected = projectPaperclipRunnerTaskConfig("codex_app_server", base, {
|
||||
provider: "opencode", model: "gpt-6-luna", managedProfileId: "other-profile",
|
||||
});
|
||||
expect(projected).toEqual({ provider: "codex", model: "gpt-6-luna", codexPermissionMode: "never" });
|
||||
expect(resolvePaperclipRunnerNativeProviderInput({
|
||||
backend: "codex_app_server", adapterConfig: projected,
|
||||
})).toEqual({ provider: "codex", model: "gpt-6-luna", codexApprovalPolicy: "never" });
|
||||
expect(projectPaperclipRunnerTaskConfig("codex_app_server", base, { modelReasoningEffort: "high" }))
|
||||
.toMatchObject({ provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "high" });
|
||||
});
|
||||
|
||||
it("uses an OpenCode task model without changing the Runner provider", () => {
|
||||
const projected = projectPaperclipRunnerTaskConfig(
|
||||
"opencode_server",
|
||||
{ provider: "opencode", model: "openrouter/qwen/qwen3-coder-next", opencodePermissionMode: "deny" },
|
||||
{ provider: "codex", model: "openrouter/deepseek/deepseek-v4-flash-0731", modelReasoningEffort: "ultra" },
|
||||
);
|
||||
expect(projected).toEqual({
|
||||
provider: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", opencodePermissionMode: "deny",
|
||||
});
|
||||
expect(resolvePaperclipRunnerNativeProviderInput({ backend: "opencode_server", adapterConfig: projected }))
|
||||
.toEqual({
|
||||
provider: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", opencodePermissionMode: "deny",
|
||||
});
|
||||
});
|
||||
|
||||
it("requires persisted Claude recovery to use the qualified identity and current profile secret", () => {
|
||||
const stored = {
|
||||
id: "00000000-0000-4000-8000-000000000001",
|
||||
|
||||
@@ -253,6 +253,7 @@ import {
|
||||
import {
|
||||
assertAgentCoreProfileRecoveryBinding,
|
||||
assertManagedProfileRecoveryBinding,
|
||||
projectPaperclipRunnerTaskConfig,
|
||||
resolvePaperclipRunnerNativeProviderInput,
|
||||
} from "./native-runtime/provider-profile.js";
|
||||
import {
|
||||
@@ -23771,7 +23772,14 @@ export function heartbeatService(
|
||||
: null,
|
||||
...resolvePaperclipRunnerNativeProviderInput({
|
||||
backend: nativeRuntimeResolution.profile.backend,
|
||||
adapterConfig: agent.adapterConfig,
|
||||
adapterConfig: nativeRuntimeResolution.profile.backend === "codex_app_server"
|
||||
|| nativeRuntimeResolution.profile.backend === "opencode_server"
|
||||
? projectPaperclipRunnerTaskConfig(
|
||||
nativeRuntimeResolution.profile.backend,
|
||||
agent.adapterConfig,
|
||||
issueAssigneeOverrides?.adapterConfig,
|
||||
)
|
||||
: agent.adapterConfig,
|
||||
managedProfile,
|
||||
agentCoreProfile,
|
||||
}),
|
||||
|
||||
@@ -18,11 +18,14 @@ describe("LCA-05 explicit native work mode", () => {
|
||||
taskPrompt: text,
|
||||
workspace: { id: "50000000-0000-4000-8000-000000000005", cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null },
|
||||
normalizedSessionId: null,
|
||||
provider: "codex",
|
||||
codexReasoningEffort: "high",
|
||||
planningContext: workMode === "planning" ? { documentId: null, baseRevisionId: null, baseRevisionNumber: 0, markdown: "", sha256: "a".repeat(64), reviewContext: {} } : null,
|
||||
completionContract: { id: "70000000-0000-4000-8000-000000000007", sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.run-result.v1", contract: { revision: "1", objective: "Deliver the requested work", criteria: [{ id: "output", requirement: "Deliver the requested work" }] } },
|
||||
runtimeContext: nativeRuntimeContextFixture(),
|
||||
});
|
||||
expect(input.task.workMode).toBe(workMode);
|
||||
expect(input.provider).toMatchObject({ kind: "codex", reasoningEffort: "high" });
|
||||
expect(input.executionMode).toBe(workMode === "planning" ? "plan" : "default");
|
||||
expect(input.task.title).toBe(text);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export function buildNativeExecutionInput(input: {
|
||||
provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx";
|
||||
acpxAgent?: NativeAcpxAgent;
|
||||
codexApprovalPolicy?: NativeCodexApprovalPolicy;
|
||||
codexReasoningEffort?: string;
|
||||
opencodePermissionMode?: NativeOpenCodePermissionMode;
|
||||
acpxPermissionMode?: NativeAcpxPermissionMode;
|
||||
model?: string | null;
|
||||
@@ -281,6 +282,7 @@ export function buildNativeExecutionInput(input: {
|
||||
kind: "codex",
|
||||
model: input.model ?? null,
|
||||
approvalPolicy: input.codexApprovalPolicy ?? "never",
|
||||
...(input.codexReasoningEffort ? { reasoningEffort: input.codexReasoningEffort } : {}),
|
||||
},
|
||||
completionContract: {
|
||||
id: input.completionContract.id,
|
||||
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
resolvePaperclipRunnerPermissionMode,
|
||||
type PaperclipRunnerProvider,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
codexLocalReasoningEffortsForModel,
|
||||
isCodexLocalKnownModel,
|
||||
} from "@paperclipai/adapter-codex-local";
|
||||
import {
|
||||
AGENTCORE_QUALIFIED_MODEL,
|
||||
CLAUDE_MANAGED_QUALIFIED_MODEL,
|
||||
@@ -63,6 +67,7 @@ export type PaperclipRunnerNativeProviderInput =
|
||||
provider: "codex";
|
||||
model: string | null;
|
||||
codexApprovalPolicy: "never" | "on-request" | "untrusted";
|
||||
codexReasoningEffort?: string;
|
||||
}
|
||||
| {
|
||||
provider: "opencode";
|
||||
@@ -138,6 +143,29 @@ function optionalString(value: unknown): string | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
/** A task may change a local Runner model (and Codex effort), but not provider identity. */
|
||||
export function projectPaperclipRunnerTaskConfig(
|
||||
backend: "codex_app_server" | "opencode_server",
|
||||
agentConfig: unknown,
|
||||
taskOverrides: unknown,
|
||||
): Record<string, unknown> {
|
||||
const base = asRecord(agentConfig);
|
||||
const task = asRecord(taskOverrides);
|
||||
const config = { ...base };
|
||||
const model = optionalString(task.model);
|
||||
const effortKey = backend === "codex_app_server"
|
||||
? ["modelReasoningEffort", "reasoningEffort", "effort"].find((key) => key in task)
|
||||
: undefined;
|
||||
if (backend === "codex_app_server" && ((model !== null && model !== optionalString(base.model)) || effortKey !== undefined)) {
|
||||
delete config.modelReasoningEffort;
|
||||
delete config.reasoningEffort;
|
||||
delete config.effort;
|
||||
}
|
||||
if (model !== null) config.model = model;
|
||||
if (effortKey !== undefined) config.modelReasoningEffort = task[effortKey];
|
||||
return config;
|
||||
}
|
||||
|
||||
function positiveNumberOrNull(
|
||||
value: unknown,
|
||||
code: string,
|
||||
@@ -650,6 +678,18 @@ export function resolvePaperclipRunnerNativeProviderInput(input: {
|
||||
},
|
||||
};
|
||||
}
|
||||
const effort = config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort;
|
||||
const allowedEfforts = ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
||||
if (effort !== undefined && effort !== null && effort !== "" && (
|
||||
typeof effort !== "string" || !allowedEfforts.includes(effort)
|
||||
|| (profile.model && isCodexLocalKnownModel(profile.model)
|
||||
&& !codexLocalReasoningEffortsForModel(profile.model).includes(effort as "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra"))
|
||||
)) {
|
||||
throw new PaperclipRunnerProviderProfileError(
|
||||
"paperclip_runner_codex_effort_invalid",
|
||||
"Paperclip Runner Codex reasoning effort is not supported for this model.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
provider: "codex",
|
||||
model: profile.model,
|
||||
@@ -657,5 +697,6 @@ export function resolvePaperclipRunnerNativeProviderInput(input: {
|
||||
"codex",
|
||||
config.codexPermissionMode,
|
||||
) as "never" | "on-request" | "untrusted",
|
||||
...(typeof effort === "string" && effort ? { codexReasoningEffort: effort } : {}),
|
||||
};
|
||||
}
|
||||
@@ -136,7 +136,8 @@ export function ComposerRunSettingsPicker({
|
||||
const filteredModels = models.filter((item) =>
|
||||
`${item.label} ${item.id}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const exactMatch = models.some((item) => item.id.toLowerCase() === query.toLowerCase());
|
||||
const needsProvider = agent && ["opencode_local", "pi_local", "kimi_local"].includes(agent.adapterType);
|
||||
const needsProvider = agent && (["opencode_local", "pi_local", "kimi_local"].includes(agent.adapterType)
|
||||
|| (agent.adapterType === "paperclip_runner" && provider === "opencode"));
|
||||
const manualValid = query.length > 0 && !/\s/.test(query)
|
||||
&& (!needsProvider || /^[^/]+\/.+[^/]$/.test(query))
|
||||
&& (provider !== "openrouter" || /^openrouter\/[^/]+\/.+[^/]$/.test(query));
|
||||
|
||||
@@ -12,6 +12,8 @@ const agent = (adapterType: Agent["adapterType"], provider?: string) => ({
|
||||
describe("composer run settings", () => {
|
||||
it("shows only effort levels known for the selected harness and model", () => {
|
||||
expect(composerEfforts(agent("codex_local"), "gpt-6-astra", [])).toContain("ultra");
|
||||
expect(composerEfforts(agent("paperclip_runner", "codex"), "gpt-6-astra", [])).toContain("ultra");
|
||||
expect(composerEfforts(agent("paperclip_runner", "opencode"), "gpt-6-astra", [])).toEqual([]);
|
||||
expect(composerEfforts(agent("codex_local"), "custom-private-model", [])).toEqual([]);
|
||||
expect(composerEfforts(agent("opencode_local"), "openrouter/x/y", ["openrouter/x/y"])).toEqual([]);
|
||||
expect(composerEfforts(agent("kimi_local"), "kimi-code/k3", ["kimi-code/k3"])).toEqual(["low", "high", "max"]);
|
||||
@@ -32,5 +34,8 @@ describe("composer run settings", () => {
|
||||
expect(readComposerRunSettings({ adapterConfig: { reasoningEffort: "xhigh" } }, "codex_local").effort).toBe("xhigh");
|
||||
expect(mergeComposerRunSettings(previous, "codex_local", { model: "gpt-6-astra", effort: "ultra", fast: true }, true))
|
||||
.toEqual({ adapterConfig: { model: "gpt-6-astra", modelReasoningEffort: "ultra", fastMode: true } });
|
||||
const runner = mergeComposerRunSettings(null, "paperclip_runner", { model: "gpt-6-astra", effort: "ultra", fast: false });
|
||||
expect(runner).toEqual({ adapterConfig: { model: "gpt-6-astra", modelReasoningEffort: "ultra" } });
|
||||
expect(readComposerRunSettings(runner, "paperclip_runner").effort).toBe("ultra");
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@ export function composerCatalogProvider(agent: Agent | undefined): string | unde
|
||||
|
||||
export function composerEfforts(agent: Agent | undefined, model: string, catalogIds: readonly string[]): readonly string[] {
|
||||
if (!agent || !model) return [];
|
||||
if (agent.adapterType === "codex_local") {
|
||||
if (agent.adapterType === "codex_local" || (agent.adapterType === "paperclip_runner" && composerCatalogProvider(agent) === "codex")) {
|
||||
return isCodexLocalKnownModel(model) ? codexLocalReasoningEffortsForModel(model) : [];
|
||||
}
|
||||
if (!catalogIds.includes(model)) return [];
|
||||
@@ -53,7 +53,7 @@ export function readComposerRunSettings(overrides: IssueAssigneeAdapterOverrides
|
||||
const config = overrides?.adapterConfig ?? {};
|
||||
const effortKey = composerEffortKey(adapterType);
|
||||
const effortValue = effortKey && (config[effortKey]
|
||||
?? (adapterType === "codex_local" ? config.reasoningEffort ?? config.effort : undefined));
|
||||
?? (adapterType === "codex_local" || adapterType === "paperclip_runner" ? config.reasoningEffort ?? config.effort : undefined));
|
||||
return {
|
||||
model: typeof config.model === "string" ? config.model : null,
|
||||
effort: typeof effortValue === "string" ? effortValue : null,
|
||||
@@ -62,7 +62,7 @@ export function readComposerRunSettings(overrides: IssueAssigneeAdapterOverrides
|
||||
}
|
||||
|
||||
function composerEffortKey(adapterType: string | undefined): string | null {
|
||||
if (adapterType === "codex_local") return "modelReasoningEffort";
|
||||
if (adapterType === "codex_local" || adapterType === "paperclip_runner") return "modelReasoningEffort";
|
||||
if (adapterType === "claude_local" || adapterType === "kimi_local") return "effort";
|
||||
if (adapterType === "pi_local") return "thinking";
|
||||
if (adapterType === "opencode_local") return "variant";
|
||||
|
||||
@@ -90,7 +90,7 @@ export function effortChoices(agent: ComposerAgent, model: string): readonly str
|
||||
}
|
||||
|
||||
export function fastModeAvailable(agent: ComposerAgent, model: string): boolean {
|
||||
return (agent.adapterType === "codex_local" || agent.id === "runner")
|
||||
return agent.adapterType === "codex_local"
|
||||
&& isCodexLocalKnownModel(model)
|
||||
&& isCodexLocalFastModeSupported(model);
|
||||
}
|
||||
|
||||
@@ -139,6 +139,10 @@ export const RunnerCodexProfile: Story = {
|
||||
name: "16 · Runner · Codex profile",
|
||||
args: { agentId: "runner", initialPanel: "models" },
|
||||
};
|
||||
export const RunnerCodexEffort: Story = {
|
||||
name: "16b · Runner · Codex effort",
|
||||
args: { agentId: "runner", initialPanel: "settings", initialModel: "gpt-6-astra", initialEffort: "high" },
|
||||
};
|
||||
export const GrokModelOnly: Story = {
|
||||
name: "17 · Grok · model only",
|
||||
args: { agentId: "grok", initialPanel: "settings" },
|
||||
@@ -250,6 +254,16 @@ export const ProductionComposer: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ProductionRunnerCodexEffort: Story = {
|
||||
name: "23a · App Runner picker with Codex effort",
|
||||
render: () => <ComposerRunSettingsLiveStory agentId="runner" initialPanel="settings" initialModel="gpt-6-astra" initialEffort="high" />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const screen = within(canvasElement.ownerDocument.body);
|
||||
await expect(screen.getByRole("slider", { name: "Effort" })).toBeVisible();
|
||||
await expect(screen.getByTestId("selected-effort")).toHaveTextContent("High");
|
||||
},
|
||||
};
|
||||
|
||||
export const ProductionMobileComposer: Story = {
|
||||
name: "23b · App picker on mobile",
|
||||
render: () => <ComposerRunSettingsLiveStory initialPanel="settings" mobile compact />,
|
||||
@@ -262,6 +276,17 @@ export const ProductionMobileComposer: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ProductionMobileRunnerCodexEffort: Story = {
|
||||
name: "23c · App Runner Codex effort on mobile",
|
||||
render: () => <ComposerRunSettingsLiveStory agentId="runner" initialPanel="settings" initialModel="gpt-6-astra" mobile compact />,
|
||||
globals: { viewport: { value: "mobile1", isRotated: false } },
|
||||
play: async ({ canvasElement }) => {
|
||||
const screen = within(canvasElement.ownerDocument.body);
|
||||
await expect(screen.getByTestId("composer-mobile-dialog")).toBeVisible();
|
||||
await expect(screen.getByRole("slider", { name: "Effort" })).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const IntermediateWidthPlan: Story = {
|
||||
name: "24 · Narrow desktop with Plan and open picker",
|
||||
args: { compact: true, initialMode: "planning", initialPanel: "settings" },
|
||||
|
||||
Reference in new issue
Block a user