mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02: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
@@ -150,7 +150,7 @@ Invariant: every business record belongs to exactly one company.
|
||||
- `capabilities` text null
|
||||
- `adapter_type` text; built-ins include `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `pi_local`, `cursor`, and `openclaw_gateway`
|
||||
- `adapter_config` jsonb not null
|
||||
- `runtime_config` jsonb not null default `{}`
|
||||
- `runtime_config` jsonb not null default `{}`; may include Paperclip runtime policy such as `modelProfiles.cheap.adapterConfig` for an optional low-cost model lane that does not change the primary adapter config
|
||||
- `default_environment_id` uuid fk `environments.id` null
|
||||
- `context_mode` enum: `thin | fat` default `thin`
|
||||
- `budget_monthly_cents` int not null default 0
|
||||
|
||||
@@ -20,6 +20,8 @@ export type {
|
||||
AdapterSkillContext,
|
||||
AdapterSessionCodec,
|
||||
AdapterModel,
|
||||
AdapterModelProfileKey,
|
||||
AdapterModelProfileDefinition,
|
||||
HireApprovedPayload,
|
||||
HireApprovedHookResult,
|
||||
ConfigFieldOption,
|
||||
|
||||
@@ -144,6 +144,16 @@ export interface AdapterModel {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type AdapterModelProfileKey = "cheap";
|
||||
|
||||
export interface AdapterModelProfileDefinition {
|
||||
key: AdapterModelProfileKey;
|
||||
label: string;
|
||||
description?: string;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
source?: "adapter_default" | "discovered";
|
||||
}
|
||||
|
||||
export type AdapterEnvironmentCheckLevel = "info" | "warn" | "error";
|
||||
|
||||
export interface AdapterEnvironmentCheck {
|
||||
@@ -329,6 +339,8 @@ export interface ServerAdapterModule {
|
||||
supportsLocalAgentJwt?: boolean;
|
||||
models?: AdapterModel[];
|
||||
listModels?: () => Promise<AdapterModel[]>;
|
||||
modelProfiles?: AdapterModelProfileDefinition[];
|
||||
listModelProfiles?: () => Promise<AdapterModelProfileDefinition[]>;
|
||||
/**
|
||||
* Optional explicit refresh hook for model discovery.
|
||||
* Use this when the adapter caches discovered models and needs a bypass path
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "claude_local";
|
||||
export const label = "Claude Code (local)";
|
||||
|
||||
@@ -10,6 +12,19 @@ export const models = [
|
||||
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
||||
];
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [
|
||||
{
|
||||
key: "cheap",
|
||||
label: "Cheap",
|
||||
description: "Use Claude Sonnet as the lower-cost Claude Code lane while preserving the agent's primary model.",
|
||||
adapterConfig: {
|
||||
model: "claude-sonnet-4-6",
|
||||
effort: "low",
|
||||
},
|
||||
source: "adapter_default",
|
||||
},
|
||||
];
|
||||
|
||||
export const agentConfigurationDoc = `# claude_local agent configuration
|
||||
|
||||
Adapter: claude_local
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "codex_local";
|
||||
export const label = "Codex (local)";
|
||||
|
||||
export const DEFAULT_CODEX_LOCAL_MODEL = "gpt-5.3-codex";
|
||||
export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true;
|
||||
export const CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS = ["gpt-5.4"] as const;
|
||||
@@ -40,6 +43,19 @@ export const models = [
|
||||
{ id: "codex-mini-latest", label: "Codex Mini" },
|
||||
];
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [
|
||||
{
|
||||
key: "cheap",
|
||||
label: "Cheap",
|
||||
description: "Use the lowest-cost known Codex local model lane without changing the primary model.",
|
||||
adapterConfig: {
|
||||
model: "gpt-5.3-codex-spark",
|
||||
modelReasoningEffort: "low",
|
||||
},
|
||||
source: "adapter_default",
|
||||
},
|
||||
];
|
||||
|
||||
export const agentConfigurationDoc = `# codex_local agent configuration
|
||||
|
||||
Adapter: codex_local
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "cursor";
|
||||
export const label = "Cursor CLI (local)";
|
||||
|
||||
export const DEFAULT_CURSOR_LOCAL_MODEL = "auto";
|
||||
|
||||
const CURSOR_FALLBACK_MODEL_IDS = [
|
||||
@@ -46,6 +49,18 @@ const CURSOR_FALLBACK_MODEL_IDS = [
|
||||
|
||||
export const models = CURSOR_FALLBACK_MODEL_IDS.map((id) => ({ id, label: id }));
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [
|
||||
{
|
||||
key: "cheap",
|
||||
label: "Cheap",
|
||||
description: "Use Cursor's known Codex mini model as the budget lane instead of assuming auto is cheap.",
|
||||
adapterConfig: {
|
||||
model: "gpt-5.1-codex-mini",
|
||||
},
|
||||
source: "adapter_default",
|
||||
},
|
||||
];
|
||||
|
||||
export const agentConfigurationDoc = `# cursor agent configuration
|
||||
|
||||
Adapter: cursor
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "gemini_local";
|
||||
export const label = "Gemini CLI (local)";
|
||||
|
||||
export const DEFAULT_GEMINI_LOCAL_MODEL = "auto";
|
||||
|
||||
export const models = [
|
||||
@@ -11,6 +14,18 @@ export const models = [
|
||||
{ id: "gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite" },
|
||||
];
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [
|
||||
{
|
||||
key: "cheap",
|
||||
label: "Cheap",
|
||||
description: "Use Gemini Flash Lite as the budget Gemini CLI lane while preserving the primary model.",
|
||||
adapterConfig: {
|
||||
model: "gemini-2.5-flash-lite",
|
||||
},
|
||||
source: "adapter_default",
|
||||
},
|
||||
];
|
||||
|
||||
export const agentConfigurationDoc = `# gemini_local agent configuration
|
||||
|
||||
Adapter: gemini_local
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "opencode_local";
|
||||
export const label = "OpenCode (local)";
|
||||
|
||||
@@ -11,6 +13,19 @@ export const models: Array<{ id: string; label: string }> = [
|
||||
{ id: "openai/gpt-5.1-codex-mini", label: "openai/gpt-5.1-codex-mini" },
|
||||
];
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [
|
||||
{
|
||||
key: "cheap",
|
||||
label: "Cheap",
|
||||
description: "Use OpenCode's known Codex mini model as the budget lane.",
|
||||
adapterConfig: {
|
||||
model: "openai/gpt-5.1-codex-mini",
|
||||
variant: "low",
|
||||
},
|
||||
source: "adapter_default",
|
||||
},
|
||||
];
|
||||
|
||||
export const agentConfigurationDoc = `# opencode_local agent configuration
|
||||
|
||||
Adapter: opencode_local
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "pi_local";
|
||||
export const label = "Pi (local)";
|
||||
|
||||
export const models: Array<{ id: string; label: string }> = [];
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [];
|
||||
|
||||
export const agentConfigurationDoc = `# pi_local agent configuration
|
||||
|
||||
Adapter: pi_local
|
||||
|
||||
@@ -73,6 +73,10 @@ export const AGENT_ROLE_LABELS: Record<AgentRole, string> = {
|
||||
|
||||
export const AGENT_DEFAULT_MAX_CONCURRENT_RUNS = 5;
|
||||
export const WORKSPACE_BRANCH_ROUTINE_VARIABLE = "workspaceBranch";
|
||||
|
||||
export const MODEL_PROFILE_KEYS = ["cheap"] as const;
|
||||
export type ModelProfileKey = (typeof MODEL_PROFILE_KEYS)[number];
|
||||
|
||||
export const AGENT_ICON_NAMES = [
|
||||
"bot",
|
||||
"cpu",
|
||||
|
||||
@@ -13,6 +13,7 @@ export {
|
||||
AGENT_ROLE_LABELS,
|
||||
AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
|
||||
WORKSPACE_BRANCH_ROUTINE_VARIABLE,
|
||||
MODEL_PROFILE_KEYS,
|
||||
AGENT_ICON_NAMES,
|
||||
ISSUE_STATUSES,
|
||||
INBOX_MINE_ISSUE_STATUSES,
|
||||
@@ -117,6 +118,7 @@ export {
|
||||
type AgentStatus,
|
||||
type AgentAdapterType,
|
||||
type AgentRole,
|
||||
type ModelProfileKey,
|
||||
type AgentIconName,
|
||||
type IssueStatus,
|
||||
type IssuePriority,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AgentAdapterType,
|
||||
ModelProfileKey,
|
||||
PauseReason,
|
||||
AgentRole,
|
||||
AgentStatus,
|
||||
@@ -13,6 +14,16 @@ export interface AgentPermissions {
|
||||
canCreateAgents: boolean;
|
||||
}
|
||||
|
||||
export interface AgentModelProfileConfig {
|
||||
enabled?: boolean;
|
||||
label?: string;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentRuntimeConfig extends Record<string, unknown> {
|
||||
modelProfiles?: Partial<Record<ModelProfileKey, AgentModelProfileConfig>>;
|
||||
}
|
||||
|
||||
export type AgentInstructionsBundleMode = "managed" | "external";
|
||||
|
||||
export interface AgentInstructionsFileSummary {
|
||||
@@ -72,7 +83,7 @@ export interface Agent {
|
||||
capabilities: string | null;
|
||||
adapterType: AgentAdapterType;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
runtimeConfig: Record<string, unknown>;
|
||||
runtimeConfig: AgentRuntimeConfig;
|
||||
defaultEnvironmentId?: string | null;
|
||||
budgetMonthlyCents: number;
|
||||
spentMonthlyCents: number;
|
||||
|
||||
@@ -74,7 +74,9 @@ export type {
|
||||
AgentAccessState,
|
||||
AgentChainOfCommandEntry,
|
||||
AgentDetail,
|
||||
AgentModelProfileConfig,
|
||||
AgentPermissions,
|
||||
AgentRuntimeConfig,
|
||||
AgentInstructionsBundleMode,
|
||||
AgentInstructionsFileSummary,
|
||||
AgentInstructionsFileDetail,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
IssueExecutionStateStatus,
|
||||
IssueOriginKind,
|
||||
IssuePriority,
|
||||
ModelProfileKey,
|
||||
IssueThreadInteractionContinuationPolicy,
|
||||
IssueThreadInteractionKind,
|
||||
IssueThreadInteractionStatus,
|
||||
@@ -59,6 +60,7 @@ export interface IssueLabel {
|
||||
}
|
||||
|
||||
export interface IssueAssigneeAdapterOverrides {
|
||||
modelProfile?: ModelProfileKey;
|
||||
adapterConfig?: Record<string, unknown>;
|
||||
useProjectWorkspace?: boolean;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,18 @@ export const createAgentInstructionsBundleSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const agentModelProfileConfigSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
label: z.string().trim().min(1).optional(),
|
||||
adapterConfig: z.record(z.unknown()),
|
||||
}).strict();
|
||||
|
||||
export const agentRuntimeConfigSchema = z.object({
|
||||
modelProfiles: z.object({
|
||||
cheap: agentModelProfileConfigSchema.optional(),
|
||||
}).strict().optional(),
|
||||
}).catchall(z.unknown());
|
||||
|
||||
export const createAgentSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
role: z.enum(AGENT_ROLES).optional().default("general"),
|
||||
@@ -62,7 +74,7 @@ export const createAgentSchema = z.object({
|
||||
adapterType: agentAdapterTypeSchema,
|
||||
adapterConfig: adapterConfigSchema.optional().default({}),
|
||||
instructionsBundle: createAgentInstructionsBundleSchema.optional(),
|
||||
runtimeConfig: z.record(z.unknown()).optional().default({}),
|
||||
runtimeConfig: agentRuntimeConfigSchema.optional().default({}),
|
||||
defaultEnvironmentId: z.string().uuid().optional().nullable(),
|
||||
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
|
||||
permissions: agentPermissionsSchema.optional(),
|
||||
|
||||
@@ -107,6 +107,7 @@ export {
|
||||
createAgentSchema,
|
||||
createAgentHireSchema,
|
||||
updateAgentSchema,
|
||||
agentRuntimeConfigSchema,
|
||||
agentInstructionsBundleModeSchema,
|
||||
updateAgentInstructionsBundleSchema,
|
||||
upsertAgentInstructionsFileSchema,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
updateIssueSchema,
|
||||
upsertIssueDocumentSchema,
|
||||
} from "./issue.js";
|
||||
import { createAgentSchema } from "./agent.js";
|
||||
|
||||
describe("issue validators", () => {
|
||||
it("passes real line breaks through unchanged", () => {
|
||||
@@ -93,4 +94,67 @@ describe("issue validators", () => {
|
||||
|
||||
expect(parsed.requestDepth).toBe(MAX_ISSUE_REQUEST_DEPTH);
|
||||
});
|
||||
|
||||
it("accepts the cheap model profile in issue assignee adapter overrides", () => {
|
||||
const parsed = createIssueSchema.parse({
|
||||
title: "Run a cheap heartbeat",
|
||||
assigneeAdapterOverrides: {
|
||||
modelProfile: "cheap",
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.assigneeAdapterOverrides?.modelProfile).toBe("cheap");
|
||||
});
|
||||
|
||||
it("rejects unknown issue model profile keys", () => {
|
||||
const parsed = updateIssueSchema.safeParse({
|
||||
assigneeAdapterOverrides: {
|
||||
modelProfile: "fast",
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
|
||||
it("validates agent runtime cheap model profile config without rejecting other runtime fields", () => {
|
||||
const parsed = createAgentSchema.parse({
|
||||
name: "Coder",
|
||||
adapterType: "codex_local",
|
||||
runtimeConfig: {
|
||||
heartbeat: { enabled: true },
|
||||
modelProfiles: {
|
||||
cheap: {
|
||||
enabled: true,
|
||||
label: "Cheap Codex",
|
||||
adapterConfig: {
|
||||
model: "gpt-5.3-codex-spark",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.runtimeConfig.modelProfiles?.cheap?.adapterConfig).toEqual({
|
||||
model: "gpt-5.3-codex-spark",
|
||||
});
|
||||
expect(parsed.runtimeConfig.heartbeat).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("rejects unknown agent runtime model profile keys", () => {
|
||||
const parsed = createAgentSchema.safeParse({
|
||||
name: "Coder",
|
||||
adapterType: "codex_local",
|
||||
runtimeConfig: {
|
||||
modelProfiles: {
|
||||
fast: {
|
||||
adapterConfig: {
|
||||
model: "gpt-5-mini",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES,
|
||||
ISSUE_THREAD_INTERACTION_KINDS,
|
||||
ISSUE_THREAD_INTERACTION_STATUSES,
|
||||
MODEL_PROFILE_KEYS,
|
||||
} from "../constants.js";
|
||||
import { multilineTextSchema } from "./text.js";
|
||||
|
||||
@@ -44,6 +45,7 @@ export const issueExecutionWorkspaceSettingsSchema = z
|
||||
|
||||
export const issueAssigneeAdapterOverridesSchema = z
|
||||
.object({
|
||||
modelProfile: z.enum(MODEL_PROFILE_KEYS).optional(),
|
||||
adapterConfig: z.record(z.unknown()).optional(),
|
||||
useProjectWorkspace: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@@ -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