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
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
import { ORPCError } from "@orpc/server";
|
|
import type { EncryptedSecretStore } from "@rakazo/adapters";
|
|
import { isCatalogModelChoice, parseModelSecret } from "@rakazo/adapters";
|
|
import type { Actor, ModelTierMapping, SetModelTierInput } from "@rakazo/contracts";
|
|
import { ModelTierSchema, OPENAI_COMPATIBLE_PROVIDER_ID } from "@rakazo/contracts";
|
|
import type { PrismaClient } from "@rakazo/db";
|
|
import { listSpaceModelTiers, setSpaceModelTier } from "@rakazo/db";
|
|
|
|
const NOT_CONNECTED = "Connect that model provider first";
|
|
|
|
export async function listModelTiers(
|
|
prisma: PrismaClient,
|
|
actor: Actor,
|
|
): Promise<ModelTierMapping[]> {
|
|
const rows = await listSpaceModelTiers(prisma, actor);
|
|
return rows.flatMap((row) => {
|
|
const tier = ModelTierSchema.safeParse(row.tier);
|
|
return tier.success
|
|
? [
|
|
{
|
|
tier: tier.data,
|
|
credentialId: row.credentialId,
|
|
provider: row.credential.provider,
|
|
modelId: row.modelId,
|
|
},
|
|
]
|
|
: [];
|
|
});
|
|
}
|
|
|
|
export async function setModelTier(prisma: PrismaClient, actor: Actor, input: SetModelTierInput) {
|
|
if (input.model) {
|
|
const credential = await prisma.userModelCredential.findFirst({
|
|
where: { id: input.model.credentialId, userId: actor.userId },
|
|
select: { provider: true },
|
|
});
|
|
if (!credential) throw new ORPCError("NOT_FOUND", { message: NOT_CONNECTED });
|
|
// An OpenAI-compatible server runs whatever ids it serves; other providers run catalog models.
|
|
if (
|
|
credential.provider !== OPENAI_COMPATIBLE_PROVIDER_ID &&
|
|
!isCatalogModelChoice(credential.provider, input.model.modelId)
|
|
) {
|
|
throw new ORPCError("BAD_REQUEST", { message: "Unknown model for that provider" });
|
|
}
|
|
}
|
|
await setSpaceModelTier(prisma, actor, input.tier, input.model);
|
|
return listModelTiers(prisma, actor);
|
|
}
|
|
|
|
/** The stored address and key of a connected OpenAI-compatible server, for listing its models. */
|
|
export async function connectedOpenAiCompatibleServer(
|
|
deps: { prisma: PrismaClient; secrets: Pick<EncryptedSecretStore, "load"> },
|
|
actor: Actor,
|
|
credentialId: string,
|
|
) {
|
|
const credential = await deps.prisma.userModelCredential.findFirst({
|
|
where: { id: credentialId, userId: actor.userId, provider: OPENAI_COMPATIBLE_PROVIDER_ID },
|
|
select: { secretId: true },
|
|
});
|
|
const secret = credential
|
|
? await deps.prisma.secret.findFirst({
|
|
where: { id: credential.secretId, userId: actor.userId, spaceId: null },
|
|
select: { id: true, ciphertext: true },
|
|
})
|
|
: null;
|
|
if (!secret) throw new ORPCError("NOT_FOUND", { message: NOT_CONNECTED });
|
|
let stored: ReturnType<typeof parseModelSecret>;
|
|
try {
|
|
stored = parseModelSecret(deps.secrets.load(secret.ciphertext, secret.id));
|
|
} catch {
|
|
throw new ORPCError("NOT_FOUND", { message: NOT_CONNECTED });
|
|
}
|
|
if (stored.kind !== "openai_compatible") {
|
|
throw new ORPCError("NOT_FOUND", { message: NOT_CONNECTED });
|
|
}
|
|
return { baseUrl: stored.baseUrl, apiKey: stored.apiKey };
|
|
}
|