mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 23:43:16 +01:00
Add adapter cheap model profile contract
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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();
|
||||
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
findServerAdapter,
|
||||
findActiveServerAdapter,
|
||||
detectAdapterModel,
|
||||
listAdapterModelProfiles,
|
||||
registerServerAdapter,
|
||||
unregisterServerAdapter,
|
||||
requireServerAdapter,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export type {
|
||||
AdapterSkillContext,
|
||||
AdapterSessionCodec,
|
||||
AdapterModel,
|
||||
AdapterModelProfileKey,
|
||||
AdapterModelProfileDefinition,
|
||||
NativeContextManagement,
|
||||
ResolvedSessionCompactionPolicy,
|
||||
SessionCompactionPolicy,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user