A space can now map the tiers fast, standard and code to connected models, including several models served by one OpenAI-compatible router endpoint (one credential, many model ids). A bot can ask for a tier, and a single run_subagent call can ask for one. A tier with no model mapped falls back to the usual model choice, so nothing breaks before tiers are set up. - db: SpaceModelTier repo (list, find, set or clear); tier mappings are copied into a new space like model and voice preferences; createBot carries a tier, so duplicates keep it and spawned bots inherit it. - contracts/api: models.tiers.list and models.tiers.set; models.probeOpenAiCompatible can list a connected server's models by credential id with its stored address and key; bots.update accepts modelTier and keeps a pinned model and a tier mutually exclusive, so a model picked on a surface that knows no tiers (mobile) is never silently overridden. Thinking-level validation follows the tier's model. AI consent status discloses tier models; desktop local settings allow the tier procedures. - adapters: selectConfiguredModel runs a mapped tier ahead of the pinned model and the space default; the executor resolves bot tiers for runs and model lookups, and helper tiers through resolveModelTier. run_subagent takes model_tier; an explicit model wins over it. - web: a collapsed Tiers section in model settings (shown once a model is connected) and mapped tiers as choices in the bot Model select. - docs: tiers in self-host model setup. - tests: resolution order and fallbacks (selection, executor, Pi helper), repo, API and contract units, authorization and offline Pi Postgres suites, and a model-tiers web E2E spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import type { Prisma, PrismaClient } from "./client.js";
|
|
|
|
type ModelTierScope = { userId: string; spaceId: string };
|
|
|
|
// The composite (credentialId, userId) foreign key keeps every mapped credential the user's own.
|
|
function withCredential(scope: ModelTierScope) {
|
|
return {
|
|
credential: {
|
|
include: {
|
|
preferences: {
|
|
where: { spaceId: scope.spaceId, userId: scope.userId },
|
|
select: { isDefault: true, modelId: true },
|
|
},
|
|
},
|
|
},
|
|
} as const;
|
|
}
|
|
|
|
type ModelTierRow = Prisma.SpaceModelTierGetPayload<{
|
|
include: ReturnType<typeof withCredential>;
|
|
}>;
|
|
|
|
/**
|
|
* Shape the credential like findModelCredential does. defaultModel stays the space's saved
|
|
* model, not the tier's, so capabilities kept for that saved model never spread to tier models.
|
|
*/
|
|
function toModelTier(row: ModelTierRow) {
|
|
const { preferences, ...credential } = row.credential;
|
|
return {
|
|
tier: row.tier,
|
|
credentialId: row.credentialId,
|
|
modelId: row.modelId,
|
|
credential: {
|
|
...credential,
|
|
isDefault: preferences[0]?.isDefault ?? false,
|
|
defaultModel: preferences[0]?.modelId ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function listSpaceModelTiers(
|
|
prisma: Pick<PrismaClient, "spaceModelTier">,
|
|
scope: ModelTierScope,
|
|
) {
|
|
const rows = await prisma.spaceModelTier.findMany({
|
|
where: { spaceId: scope.spaceId, userId: scope.userId },
|
|
include: withCredential(scope),
|
|
orderBy: { createdAt: "asc" },
|
|
});
|
|
return rows.map(toModelTier);
|
|
}
|
|
|
|
/** The connected model a space maps this tier to, or null when the tier is not mapped. */
|
|
export async function findSpaceModelTier(
|
|
prisma: Pick<PrismaClient, "spaceModelTier">,
|
|
scope: ModelTierScope,
|
|
tier: string,
|
|
) {
|
|
const row = await prisma.spaceModelTier.findUnique({
|
|
where: { spaceId_userId_tier: { spaceId: scope.spaceId, userId: scope.userId, tier } },
|
|
include: withCredential(scope),
|
|
});
|
|
return row ? toModelTier(row) : null;
|
|
}
|
|
|
|
export async function setSpaceModelTier(
|
|
prisma: Pick<PrismaClient, "spaceModelTier">,
|
|
scope: ModelTierScope,
|
|
tier: string,
|
|
model: { credentialId: string; modelId: string } | null,
|
|
) {
|
|
if (!model) {
|
|
await prisma.spaceModelTier.deleteMany({
|
|
where: { spaceId: scope.spaceId, userId: scope.userId, tier },
|
|
});
|
|
return;
|
|
}
|
|
await prisma.spaceModelTier.upsert({
|
|
where: { spaceId_userId_tier: { spaceId: scope.spaceId, userId: scope.userId, tier } },
|
|
create: { spaceId: scope.spaceId, userId: scope.userId, tier, ...model },
|
|
update: model,
|
|
});
|
|
}
|