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

+1 -1
View File
@@ -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
+2
View File
@@ -20,6 +20,8 @@ export type {
AdapterSkillContext,
AdapterSessionCodec,
AdapterModel,
AdapterModelProfileKey,
AdapterModelProfileDefinition,
HireApprovedPayload,
HireApprovedHookResult,
ConfigFieldOption,
+12
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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",
+2
View File
@@ -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,
+12 -1
View File
@@ -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;
+2
View File
@@ -74,7 +74,9 @@ export type {
AgentAccessState,
AgentChainOfCommandEntry,
AgentDetail,
AgentModelProfileConfig,
AgentPermissions,
AgentRuntimeConfig,
AgentInstructionsBundleMode,
AgentInstructionsFileSummary,
AgentInstructionsFileDetail,
+2
View File
@@ -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;
}
+13 -1
View File
@@ -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(),
+1
View File
@@ -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);
});
});
+2
View File
@@ -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();
+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,
};