mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
## Thinking Path > - Paperclip is an open source control plane for AI agents. > - The issue composer lets operators choose a model and effort for a task message. > - The picker must show only effort levels that the selected adapter can execute. > - The Codex Runner fix in #14568 exposed similar gaps in other adapters. > - Grok hid a supported control. Claude used one range for all models. Kimi ACP showed a control that it ignored. > - This pull request aligns the picker with each adapter and adds matching stories. > - Operators can see and save supported effort settings without false controls. ## Linked Issues or Issue Description **What happened?** The composer hid Grok effort. It showed an incomplete effort range for some Claude models and a slider for Claude Haiku. It showed Kimi effort on the default ACP engine even though that engine drops the value. **Expected behavior** The composer should show only effort levels supported by the selected model and execution engine. Grok effort should reach its adapter as `reasoningEffort`. **Steps to reproduce** 1. Select a Grok agent and `grok-4.7`. Observe that the picker has no effort slider. 2. Select Claude Sonnet 5 or Haiku 4.5. Observe the generic low to high slider. 3. Select a Kimi agent on ACP. Observe the slider even though ACP ignores effort. Related fix: #14568. ## What Changed - Use Claude and Grok adapter capability helpers in the production picker and Storybook preview. - Map Grok composer effort to `reasoningEffort` in per-message overrides. - Show Kimi effort only when its agent uses the CLI engine, including when it runs its default model. - Show Grok effort when it runs its default model. - Add design and production stories for Claude, Grok, and Kimi ACP and CLI cases. - Document the picker rules and add focused tests. ## Verification - `pnpm --filter @paperclipai/ui exec vitest run src/components/task-chat/composer-run-settings.test.ts` - `pnpm --filter @paperclipai/ui typecheck` - `pnpm check:token-gates` - `pnpm -r typecheck`, `pnpm test:run`, and `pnpm build` passed on the first commit. - The focused test, UI typecheck, token check, and Storybook build passed again after the review fixes. - Browser smoke: production stories show effort for default Grok and CLI Kimi, and hide it for ACP Kimi and Claude Haiku. ## Risks - Existing Kimi ACP agents lose a slider that could not change execution. Their stored effort override remains unchanged until the next edit. - Claude and Grok ranges follow their adapter catalogs. A provider can change model support before the catalog is updated. ## Model Used OpenAI Codex based on GPT-6. The exact runtime model ID and context window are not exposed in this session. Reasoning, tool use, and code execution were used. ## 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>
103 lines
5.5 KiB
TypeScript
103 lines
5.5 KiB
TypeScript
import { codexLocalReasoningEffortsForModel, isCodexLocalFastModeSupported, isCodexLocalKnownModel } from "@paperclipai/adapter-codex-local";
|
|
import { claudeLocalReasoningEffortsForModel } from "@paperclipai/adapter-claude-local";
|
|
import { DEFAULT_GROK_LOCAL_MODEL, grokLocalReasoningEffortsForModel } from "@paperclipai/adapter-grok-local";
|
|
import { DEFAULT_KIMI_LOCAL_MODEL, modelSupportsEffort, KIMI_SUPPORTED_EFFORTS } from "@paperclipai/adapter-kimi-local";
|
|
import { aiConnectionBindingSchema, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
|
|
|
|
export interface ComposerRunSettings {
|
|
model: string | null;
|
|
effort: string | null;
|
|
fast: boolean;
|
|
}
|
|
|
|
export const DEFAULT_COMPOSER_RUN_SETTINGS: ComposerRunSettings = { model: null, effort: null, fast: false };
|
|
export const EFFORT_LABELS: Record<string, string> = {
|
|
off: "Off", minimal: "Minimal", low: "Low", medium: "Medium", high: "High",
|
|
xhigh: "Extra High", max: "Max", ultra: "Ultra",
|
|
};
|
|
|
|
const MODEL_ADAPTERS = new Set([
|
|
"claude_local", "codex_local", "opencode_local", "pi_local", "kimi_local",
|
|
"gemini_local", "cursor", "cursor_cloud", "grok_local", "hermes_local", "paperclip_runner",
|
|
]);
|
|
|
|
export function supportsComposerModel(agent: Agent | undefined): boolean {
|
|
return Boolean(agent && MODEL_ADAPTERS.has(agent.adapterType));
|
|
}
|
|
|
|
export function composerCatalogProvider(agent: Agent | undefined): string | undefined {
|
|
if (!agent) return undefined;
|
|
if (agent.adapterType === "paperclip_runner") return String(agent.adapterConfig.provider ?? "codex");
|
|
if (agent.adapterType !== "opencode_local") return undefined;
|
|
const binding = aiConnectionBindingSchema.safeParse(agent.runtimeConfig?.aiConnection).data;
|
|
const configuredModel = agent.adapterConfig.model;
|
|
return binding?.provider === "openrouter" || typeof configuredModel === "string" && configuredModel.startsWith("openrouter/")
|
|
? "openrouter" : undefined;
|
|
}
|
|
|
|
export function composerEfforts(agent: Agent | undefined, model: string, catalogIds: readonly string[]): readonly string[] {
|
|
if (!agent) return [];
|
|
const effectiveModel = model.trim() || (agent.adapterType === "grok_local" ? DEFAULT_GROK_LOCAL_MODEL
|
|
: agent.adapterType === "kimi_local" ? DEFAULT_KIMI_LOCAL_MODEL : "");
|
|
if (!effectiveModel) return [];
|
|
if (agent.adapterType === "codex_local" || (agent.adapterType === "paperclip_runner" && composerCatalogProvider(agent) === "codex")) {
|
|
return isCodexLocalKnownModel(effectiveModel) ? codexLocalReasoningEffortsForModel(effectiveModel) : [];
|
|
}
|
|
if (!catalogIds.includes(effectiveModel)) return [];
|
|
if (agent.adapterType === "claude_local") return claudeLocalReasoningEffortsForModel(effectiveModel);
|
|
if (agent.adapterType === "grok_local") return grokLocalReasoningEffortsForModel(effectiveModel);
|
|
if (agent.adapterType === "pi_local") return ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
if (agent.adapterType === "kimi_local" && typeof agent.adapterConfig.engine === "string"
|
|
&& agent.adapterConfig.engine.trim().toLowerCase() === "cli" && modelSupportsEffort(effectiveModel)) return KIMI_SUPPORTED_EFFORTS;
|
|
return [];
|
|
}
|
|
|
|
export function composerFastAvailable(agent: Agent | undefined, model: string): boolean {
|
|
return Boolean(agent && agent.adapterType === "codex_local" && isCodexLocalKnownModel(model) && isCodexLocalFastModeSupported(model));
|
|
}
|
|
|
|
export function readComposerRunSettings(overrides: IssueAssigneeAdapterOverrides | null | undefined, adapterType: string | undefined): ComposerRunSettings {
|
|
const config = overrides?.adapterConfig ?? {};
|
|
const effortKey = composerEffortKey(adapterType);
|
|
const effortValue = effortKey && (config[effortKey]
|
|
?? (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,
|
|
fast: adapterType === "codex_local" && config.fastMode === true,
|
|
};
|
|
}
|
|
|
|
function composerEffortKey(adapterType: string | undefined): string | null {
|
|
if (adapterType === "codex_local" || adapterType === "paperclip_runner") return "modelReasoningEffort";
|
|
if (adapterType === "claude_local" || adapterType === "kimi_local") return "effort";
|
|
if (adapterType === "grok_local") return "reasoningEffort";
|
|
if (adapterType === "pi_local") return "thinking";
|
|
if (adapterType === "opencode_local") return "variant";
|
|
return null;
|
|
}
|
|
|
|
export function mergeComposerRunSettings(
|
|
previous: IssueAssigneeAdapterOverrides | null | undefined,
|
|
adapterType: string | undefined,
|
|
settings: ComposerRunSettings,
|
|
reassigned = false,
|
|
): IssueAssigneeAdapterOverrides | null {
|
|
const config = { ...(reassigned ? {} : previous?.adapterConfig) };
|
|
delete config.model;
|
|
delete config.modelReasoningEffort;
|
|
delete config.reasoningEffort;
|
|
delete config.effort;
|
|
delete config.thinking;
|
|
delete config.variant;
|
|
delete config.fastMode;
|
|
if (settings.model) config.model = settings.model;
|
|
const effortKey = composerEffortKey(adapterType);
|
|
if (settings.effort && effortKey) config[effortKey] = settings.effort;
|
|
if (settings.fast && adapterType === "codex_local") config.fastMode = true;
|
|
const useProjectWorkspace = reassigned ? undefined : previous?.useProjectWorkspace;
|
|
return Object.keys(config).length || useProjectWorkspace !== undefined
|
|
? { ...(Object.keys(config).length ? { adapterConfig: config } : {}), ...(useProjectWorkspace !== undefined ? { useProjectWorkspace } : {}) }
|
|
: null;
|
|
}
|