Add adapter cheap model profile contract

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-30 14:21:37 -05:00
1 parent c4269bab59
commit 0c6fde773b
25 files changed
+323 -9

No files matched your search

@@ -28,6 +28,7 @@ import {
findActiveServerAdapter,
findServerAdapter,
listAdapterModels,
listAdapterModelProfiles,
registerServerAdapter,
requireServerAdapter,
unregisterServerAdapter,
@@ -79,6 +80,31 @@ describe("server adapter registry", () => {
]);
});
it("exposes adapter model profiles when adapters declare them", async () => {
const adapterWithProfiles: ServerAdapterModule = {
...externalAdapter,
modelProfiles: [
{
key: "cheap",
label: "Cheap",
adapterConfig: { model: "external-mini" },
source: "adapter_default",
},
],
};
registerServerAdapter(adapterWithProfiles);
expect(await listAdapterModelProfiles("external_test")).toEqual([
{
key: "cheap",
label: "Cheap",
adapterConfig: { model: "external-mini" },
source: "adapter_default",
},
]);
});
it("removes external adapters when unregistered", () => {
registerServerAdapter(externalAdapter);
@@ -167,6 +193,45 @@ describe("server adapter registry", () => {
expect(adapter!.supportsLocalAgentJwt).toBe(true);
});
it("built-in local adapters declare cheap model profile defaults where supported", async () => {
await expect(listAdapterModelProfiles("claude_local")).resolves.toEqual([
expect.objectContaining({
key: "cheap",
adapterConfig: expect.objectContaining({ model: "claude-sonnet-4-6" }),
source: "adapter_default",
}),
]);
await expect(listAdapterModelProfiles("codex_local")).resolves.toEqual([
expect.objectContaining({
key: "cheap",
adapterConfig: expect.objectContaining({ model: "gpt-5.3-codex-spark" }),
source: "adapter_default",
}),
]);
await expect(listAdapterModelProfiles("gemini_local")).resolves.toEqual([
expect.objectContaining({
key: "cheap",
adapterConfig: expect.objectContaining({ model: "gemini-2.5-flash-lite" }),
source: "adapter_default",
}),
]);
await expect(listAdapterModelProfiles("opencode_local")).resolves.toEqual([
expect.objectContaining({
key: "cheap",
adapterConfig: expect.objectContaining({ model: "openai/gpt-5.1-codex-mini" }),
source: "adapter_default",
}),
]);
await expect(listAdapterModelProfiles("cursor")).resolves.toEqual([
expect.objectContaining({
key: "cheap",
adapterConfig: expect.objectContaining({ model: "gpt-5.1-codex-mini" }),
source: "adapter_default",
}),
]);
await expect(listAdapterModelProfiles("pi_local")).resolves.toEqual([]);
});
it("switches active adapter behavior back to the builtin when an override is paused", async () => {
const builtIn = findServerAdapter("claude_local");
expect(builtIn).not.toBeNull();
+1
View File
@@ -6,6 +6,7 @@ export {
findServerAdapter,
findActiveServerAdapter,
detectAdapterModel,
listAdapterModelProfiles,
registerServerAdapter,
unregisterServerAdapter,
requireServerAdapter,
+39 -5
View File
@@ -1,4 +1,4 @@
import type { ServerAdapterModule } from "./types.js";
import type { AdapterModelProfileDefinition, ServerAdapterModule } from "./types.js";
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
import {
execute as claudeExecute,
@@ -9,7 +9,11 @@ import {
sessionCodec as claudeSessionCodec,
getQuotaWindows as claudeGetQuotaWindows,
} from "@paperclipai/adapter-claude-local/server";
import { agentConfigurationDoc as claudeAgentConfigurationDoc, models as claudeModels } from "@paperclipai/adapter-claude-local";
import {
agentConfigurationDoc as claudeAgentConfigurationDoc,
models as claudeModels,
modelProfiles as claudeModelProfiles,
} from "@paperclipai/adapter-claude-local";
import {
execute as codexExecute,
listCodexSkills,
@@ -18,7 +22,11 @@ import {
sessionCodec as codexSessionCodec,
getQuotaWindows as codexGetQuotaWindows,
} from "@paperclipai/adapter-codex-local/server";
import { agentConfigurationDoc as codexAgentConfigurationDoc, models as codexModels } from "@paperclipai/adapter-codex-local";
import {
agentConfigurationDoc as codexAgentConfigurationDoc,
models as codexModels,
modelProfiles as codexModelProfiles,
} from "@paperclipai/adapter-codex-local";
import {
execute as cursorExecute,
listCursorSkills,
@@ -26,7 +34,11 @@ import {
testEnvironment as cursorTestEnvironment,
sessionCodec as cursorSessionCodec,
} from "@paperclipai/adapter-cursor-local/server";
import { agentConfigurationDoc as cursorAgentConfigurationDoc, models as cursorModels } from "@paperclipai/adapter-cursor-local";
import {
agentConfigurationDoc as cursorAgentConfigurationDoc,
models as cursorModels,
modelProfiles as cursorModelProfiles,
} from "@paperclipai/adapter-cursor-local";
import {
execute as geminiExecute,
listGeminiSkills,
@@ -34,7 +46,11 @@ import {
testEnvironment as geminiTestEnvironment,
sessionCodec as geminiSessionCodec,
} from "@paperclipai/adapter-gemini-local/server";
import { agentConfigurationDoc as geminiAgentConfigurationDoc, models as geminiModels } from "@paperclipai/adapter-gemini-local";
import {
agentConfigurationDoc as geminiAgentConfigurationDoc,
models as geminiModels,
modelProfiles as geminiModelProfiles,
} from "@paperclipai/adapter-gemini-local";
import {
execute as openCodeExecute,
listOpenCodeSkills,
@@ -46,6 +62,7 @@ import {
import {
agentConfigurationDoc as openCodeAgentConfigurationDoc,
models as openCodeModels,
modelProfiles as openCodeModelProfiles,
} from "@paperclipai/adapter-opencode-local";
import {
execute as openclawGatewayExecute,
@@ -67,6 +84,7 @@ import {
} from "@paperclipai/adapter-pi-local/server";
import {
agentConfigurationDoc as piAgentConfigurationDoc,
modelProfiles as piModelProfiles,
} from "@paperclipai/adapter-pi-local";
import {
execute as hermesExecute,
@@ -126,6 +144,7 @@ const claudeLocalAdapter: ServerAdapterModule = {
sessionCodec: claudeSessionCodec,
sessionManagement: getAdapterSessionManagement("claude_local") ?? undefined,
models: claudeModels,
modelProfiles: claudeModelProfiles,
listModels: listClaudeModels,
supportsLocalAgentJwt: true,
supportsInstructionsBundle: true,
@@ -144,6 +163,7 @@ const codexLocalAdapter: ServerAdapterModule = {
sessionCodec: codexSessionCodec,
sessionManagement: getAdapterSessionManagement("codex_local") ?? undefined,
models: codexModels,
modelProfiles: codexModelProfiles,
listModels: listCodexModels,
refreshModels: refreshCodexModels,
supportsLocalAgentJwt: true,
@@ -163,6 +183,7 @@ const cursorLocalAdapter: ServerAdapterModule = {
sessionCodec: cursorSessionCodec,
sessionManagement: getAdapterSessionManagement("cursor") ?? undefined,
models: cursorModels,
modelProfiles: cursorModelProfiles,
listModels: listCursorModels,
supportsLocalAgentJwt: true,
supportsInstructionsBundle: true,
@@ -180,6 +201,7 @@ const geminiLocalAdapter: ServerAdapterModule = {
sessionCodec: geminiSessionCodec,
sessionManagement: getAdapterSessionManagement("gemini_local") ?? undefined,
models: geminiModels,
modelProfiles: geminiModelProfiles,
supportsLocalAgentJwt: true,
supportsInstructionsBundle: true,
instructionsPathKey: "instructionsFilePath",
@@ -206,6 +228,7 @@ const openCodeLocalAdapter: ServerAdapterModule = {
syncSkills: syncOpenCodeSkills,
sessionCodec: openCodeSessionCodec,
models: openCodeModels,
modelProfiles: openCodeModelProfiles,
sessionManagement: getAdapterSessionManagement("opencode_local") ?? undefined,
listModels: listOpenCodeModels,
supportsLocalAgentJwt: true,
@@ -224,6 +247,7 @@ const piLocalAdapter: ServerAdapterModule = {
sessionCodec: piSessionCodec,
sessionManagement: getAdapterSessionManagement("pi_local") ?? undefined,
models: [],
modelProfiles: piModelProfiles,
listModels: listPiModels,
supportsLocalAgentJwt: true,
supportsInstructionsBundle: true,
@@ -474,6 +498,16 @@ export async function refreshAdapterModels(type: string): Promise<{ id: string;
return adapter.models ?? [];
}
export async function listAdapterModelProfiles(type: string): Promise<AdapterModelProfileDefinition[]> {
const adapter = findActiveServerAdapter(type);
if (!adapter) return [];
if (adapter.listModelProfiles) {
const discovered = await adapter.listModelProfiles();
if (discovered.length > 0) return discovered;
}
return adapter.modelProfiles ?? [];
}
export function listServerAdapters(): ServerAdapterModule[] {
return Array.from(adaptersByType.values());
}
+2
View File
@@ -22,6 +22,8 @@ export type {
AdapterSkillContext,
AdapterSessionCodec,
AdapterModel,
AdapterModelProfileKey,
AdapterModelProfileDefinition,
NativeContextManagement,
ResolvedSessionCompactionPolicy,
SessionCompactionPolicy,
+2
View File
@@ -66,6 +66,7 @@ interface AdapterCapabilities {
supportsSkills: boolean;
supportsLocalAgentJwt: boolean;
requiresMaterializedRuntimeSkills: boolean;
supportsModelProfiles: boolean;
}
interface AdapterInfo {
@@ -119,6 +120,7 @@ function buildAdapterCapabilities(adapter: ServerAdapterModule): AdapterCapabili
supportsSkills: Boolean(adapter.listSkills || adapter.syncSkills),
supportsLocalAgentJwt: adapter.supportsLocalAgentJwt ?? false,
requiresMaterializedRuntimeSkills: adapter.requiresMaterializedRuntimeSkills ?? false,
supportsModelProfiles: Boolean(adapter.modelProfiles?.length || adapter.listModelProfiles),
};
}
+9
View File
@@ -63,6 +63,7 @@ import {
findActiveServerAdapter,
findServerAdapter,
listAdapterModels,
listAdapterModelProfiles,
refreshAdapterModels,
requireServerAdapter,
} from "../adapters/index.js";
@@ -1064,6 +1065,14 @@ export function agentRoutes(
res.json(models);
});
router.get("/companies/:companyId/adapters/:type/model-profiles", async (req, res) => {
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);
const type = assertKnownAdapterType(req.params.type as string);
const profiles = await listAdapterModelProfiles(type);
res.json(profiles);
});
router.get("/companies/:companyId/adapters/:type/detect-model", async (req, res) => {
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);
+8 -1
View File
@@ -8,11 +8,13 @@ import type { Db } from "@paperclipai/db";
import {
AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY,
MODEL_PROFILE_KEYS,
isEnvironmentDriverSupportedForAdapter,
type BillingType,
type EnvironmentLeaseStatus,
type ExecutionWorkspace,
type ExecutionWorkspaceConfig,
type ModelProfileKey,
type RunLivenessState,
} from "@paperclipai/shared";
import {
@@ -879,6 +881,7 @@ type SessionCompactionDecision = {
};
interface ParsedIssueAssigneeAdapterOverrides {
modelProfile: ModelProfileKey | null;
adapterConfig: Record<string, unknown> | null;
useProjectWorkspace: boolean | null;
}
@@ -1259,6 +1262,9 @@ function parseIssueAssigneeAdapterOverrides(
raw: unknown,
): ParsedIssueAssigneeAdapterOverrides | null {
const parsed = parseObject(raw);
const modelProfile = MODEL_PROFILE_KEYS.includes(parsed.modelProfile as ModelProfileKey)
? parsed.modelProfile as ModelProfileKey
: null;
const parsedAdapterConfig = parseObject(parsed.adapterConfig);
const adapterConfig =
Object.keys(parsedAdapterConfig).length > 0 ? parsedAdapterConfig : null;
@@ -1266,8 +1272,9 @@ function parseIssueAssigneeAdapterOverrides(
typeof parsed.useProjectWorkspace === "boolean"
? parsed.useProjectWorkspace
: null;
if (!adapterConfig && useProjectWorkspace === null) return null;
if (!modelProfile && !adapterConfig && useProjectWorkspace === null) return null;
return {
modelProfile,
adapterConfig,
useProjectWorkspace,
};