Files
PaperClipAI/ui/src/components/task-chat/composer-run-settings.ts
T
DottaandPaperclip 6fd56c445d feat: default Claude to the native runner on Apple Silicon
Carry Claude effort through versioned native admission and recovery. Preserve existing connections and saved runner choices, and price Opus 5.5 API receipts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 10:48:21 -05:00

129 lines
7.2 KiB
TypeScript

import { codexLocalReasoningEffortsForModel, isCodexLocalFastModeSupported, isCodexLocalKnownModel } from "@paperclipai/adapter-codex-local";
import { claudeLocalReasoningEffortsForModel } from "@paperclipai/adapter-claude-local";
import { resolvePaperclipRunnerModel } from "@paperclipai/adapter-utils";
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, aiRuntimeConnectionBindingSchema, agentHarnessType, 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));
}
/** Resolve visible defaults; host environment and a local CLI's own config are unknown. */
export function composerDefaultModel(agent: Agent | undefined): string {
if (!agent) return "";
const config = agent.adapterConfig;
const configured = typeof config.model === "string" ? config.model.trim() : "";
if (configured) return configured;
if (agent.adapterType === "claude_local") {
const env = config.env && typeof config.env === "object" && !Array.isArray(config.env)
? config.env as Record<string, unknown> : {};
// The server merges host env too, which may select a different model or
// Bedrock/Vertex. Without an explicit agent setting, the default is unknown.
return typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL.trim() : "";
}
if (agent.adapterType === "grok_local") return DEFAULT_GROK_LOCAL_MODEL;
if (agent.adapterType === "kimi_local") return DEFAULT_KIMI_LOCAL_MODEL;
if (agent.adapterType === "paperclip_runner") {
const provider = composerCatalogProvider(agent);
if (provider === "codex" || provider === "opencode") return resolvePaperclipRunnerModel(provider, config.model);
if (provider === "acpx" && config.acpxAgent !== "grok") return resolvePaperclipRunnerModel(provider, config.model);
}
return "";
}
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 [];
const pool = aiRuntimeConnectionBindingSchema.safeParse(agent.runtimeConfig?.aiConnection).data?.mode === "router";
if (pool && agent.adapterType === "paperclip_runner") return isCodexLocalKnownModel(effectiveModel) ? codexLocalReasoningEffortsForModel(effectiveModel) : /^claude-/.test(effectiveModel) ? claudeLocalReasoningEffortsForModel(effectiveModel) : [];
if (agent.adapterType === "codex_local" || (agent.adapterType === "paperclip_runner" && composerCatalogProvider(agent) === "codex")) {
return isCodexLocalKnownModel(effectiveModel) ? codexLocalReasoningEffortsForModel(effectiveModel) : [];
}
if (!catalogIds.includes(effectiveModel)) return [];
if (agentHarnessType(agent.adapterType, agent.adapterConfig) === "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;
}