diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index 653c07e936..2690cf5fbe 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -116,6 +116,7 @@ export { PAPERCLIP_RUNNER_IDLE_TIMEOUT_DEFAULT_MS, PAPERCLIP_RUNNER_IDLE_TIMEOUT_MAX_MS, PAPERCLIP_RUNNER_DEFAULT_MODELS, + PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS, PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES, isPaperclipRunnerProvider, resolvePaperclipRunnerIdleTimeoutMs, diff --git a/packages/adapter-utils/src/paperclip-runner-permissions.ts b/packages/adapter-utils/src/paperclip-runner-permissions.ts index 306cbffa74..e7bfea9329 100644 --- a/packages/adapter-utils/src/paperclip-runner-permissions.ts +++ b/packages/adapter-utils/src/paperclip-runner-permissions.ts @@ -16,6 +16,12 @@ export const PAPERCLIP_RUNNER_DEFAULT_MODELS = { opencode: "openrouter/deepseek/deepseek-v4-flash-0731", } as const; +export const PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS = { + claude: PAPERCLIP_RUNNER_DEFAULT_MODELS.acpx, + codex: PAPERCLIP_RUNNER_DEFAULT_MODELS.codex, + pi: "openrouter/deepseek/deepseek-v4-flash-0731", +} as const; + export interface PaperclipRunnerPermissionOption< TMode extends string = string, > { diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 93bab9ae63..83208bf211 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -15,6 +15,7 @@ import { redactCommandText } from "./command-redaction.js"; import { paperclipChatFilePreparationDelivery } from "./chat-file-delivery.js"; import { PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES, + PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS, resolvePaperclipRunnerModel, normalizeLegacyRunnerProvider, } from "./paperclip-runner-permissions.js"; @@ -4113,7 +4114,11 @@ export function normalizePaperclipRunnerAdapterConfig( } if (next.provider === "acpx") { next.acpxAgent ??= "claude"; - next.model = resolvePaperclipRunnerModel("acpx", config.model); + const defaultModel = next.acpxAgent === "pi" + ? PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS.pi + : PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS.claude; + next.model = typeof config.model === "string" && config.model.trim().length > 0 + ? config.model.trim() : defaultModel; } return normalizePaperclipOperationalSkillPreference(adapterType, next); } diff --git a/server/src/__tests__/agents-paperclip-runner-skills.test.ts b/server/src/__tests__/agents-paperclip-runner-skills.test.ts index eb7bb633ac..41e826a00f 100644 --- a/server/src/__tests__/agents-paperclip-runner-skills.test.ts +++ b/server/src/__tests__/agents-paperclip-runner-skills.test.ts @@ -5,6 +5,7 @@ import { PAPERCLIP_OPERATIONAL_SKILL_KEY, resolveLegacyPaperclipDesiredSkillNames, } from "@paperclipai/adapter-utils/server-utils"; +import { resolvePaperclipRunnerProviderProfile } from "../services/native-runtime/provider-profile.js"; const legacyConfig = { paperclipSkillSync: { @@ -13,6 +14,16 @@ const legacyConfig = { }; describe("paperclip_runner operational skill normalization", () => { + it.each([undefined, "", " "])("qualifies Pi's default model at persistence and execution (%s)", (model) => { + const input = { provider: "acpx", acpxAgent: "pi", model }; + const normalized = normalizePaperclipRunnerAdapterConfig("paperclip_runner", input); + expect(normalized.model).toBe("openrouter/deepseek/deepseek-v4-flash-0731"); + expect(resolvePaperclipRunnerProviderProfile(normalized)).toMatchObject({ provider: "acpx", acpxAgent: "pi", model: normalized.model }); + expect(resolvePaperclipRunnerProviderProfile(input)).toMatchObject({ provider: "acpx", acpxAgent: "pi", model: normalized.model }); + const explicit = normalizePaperclipRunnerAdapterConfig("paperclip_runner", { ...input, model: "unqualified-model" }); + expect(explicit.model).toBe("unqualified-model"); + expect(() => resolvePaperclipRunnerProviderProfile(explicit)).toThrow("requires exact model"); + }); it("applies full-auto native runner defaults at persistence boundaries", () => { expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", {})).toEqual({ provider: "codex", diff --git a/server/src/services/native-runtime/provider-profile.ts b/server/src/services/native-runtime/provider-profile.ts index 19064f197b..2eb5f96656 100644 --- a/server/src/services/native-runtime/provider-profile.ts +++ b/server/src/services/native-runtime/provider-profile.ts @@ -1,6 +1,7 @@ import { isPaperclipRunnerProvider, PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES, + PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS, resolvePaperclipRunnerPermissionMode, type PaperclipRunnerProvider, } from "@paperclipai/adapter-utils"; @@ -14,11 +15,7 @@ export const DEFAULT_OPENCODE_RUNNER_MODEL = "openrouter/deepseek/deepseek-v4-flash-0731" as const; export const CLAUDE_MANAGED_BETA_VERSION = "managed-agents-2026-04-01" as const; -export const QUALIFIED_ACPX_RUNNER_MODELS = { - claude: "claude-sonnet-5", - codex: "gpt-5.6-sol", - pi: "openrouter/deepseek/deepseek-v4-flash-0731", -} as const; +export const QUALIFIED_ACPX_RUNNER_MODELS = PAPERCLIP_RUNNER_ACPX_DEFAULT_MODELS; export type QualifiedPaperclipRunnerAcpxAgent = keyof typeof QUALIFIED_ACPX_RUNNER_MODELS; @@ -412,7 +409,7 @@ export function resolvePaperclipRunnerProviderProfile( ); } const qualifiedModel = QUALIFIED_ACPX_RUNNER_MODELS[acpxAgent]; - if (acpxAgent !== "claude" && model !== qualifiedModel) { + if (acpxAgent !== "claude" && model !== null && model !== qualifiedModel) { throw new PaperclipRunnerProviderProfileError( "paperclip_runner_acpx_model_unqualified", `Paperclip Runner ACPX ${acpxAgent} requires exact model ${qualifiedModel}.`,