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
238 lines
7.8 KiB
TypeScript
238 lines
7.8 KiB
TypeScript
import type { Actor } from "@rakazo/contracts";
|
|
import type { PrismaClient } from "@rakazo/db";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { selectConfiguredModel, validateConnectedModelChoice } from "./model-selection.js";
|
|
|
|
type SelectionInput = Parameters<typeof selectConfiguredModel>[0];
|
|
|
|
function credential(provider: string, defaultModel: string | null) {
|
|
return {
|
|
id: `credential-${provider}`,
|
|
userId: "user-1",
|
|
provider,
|
|
label: provider,
|
|
secretId: `secret-${provider}`,
|
|
supportsImages: false,
|
|
createdAt: new Date(0),
|
|
updatedAt: new Date(0),
|
|
isDefault: false,
|
|
defaultModel,
|
|
};
|
|
}
|
|
|
|
const spaceCredential = credential("space-provider", "space-model");
|
|
const overrideCredential = credential("bot-provider", "stored-model");
|
|
// One router connection serves every tier model; its saved model is a different one.
|
|
const routerCredential = credential("openai-compatible", "router-default");
|
|
const tierModel = { modelId: "router-code", credential: routerCredential };
|
|
const bot = { modelProvider: "bot-provider", modelId: "bot-model", thinkingLevel: "high" };
|
|
const defaults: SelectionInput = {
|
|
bot: null,
|
|
overrideCredential: null,
|
|
defaultCredential: spaceCredential,
|
|
settings: { defaultModelProvider: "settings-provider", defaultModelId: "settings-model" },
|
|
deployment: { provider: "deployment-provider", model: "deployment-model" },
|
|
};
|
|
|
|
describe("configured model selection", () => {
|
|
it.each<{
|
|
name: string;
|
|
input: Partial<SelectionInput>;
|
|
expected: ReturnType<typeof selectConfiguredModel>;
|
|
}>([
|
|
{
|
|
name: "uses the bot's model with its own credential",
|
|
input: { bot, overrideCredential },
|
|
expected: {
|
|
provider: "bot-provider",
|
|
id: "bot-model",
|
|
credential: overrideCredential,
|
|
thinkingLevel: "high",
|
|
},
|
|
},
|
|
{
|
|
name: "drops override thinking when its provider has no credential",
|
|
input: { bot },
|
|
expected: {
|
|
provider: "space-provider",
|
|
id: "space-model",
|
|
credential: spaceCredential,
|
|
thinkingLevel: null,
|
|
},
|
|
},
|
|
{
|
|
name: "keeps bot thinking with the Space default",
|
|
input: { bot: { modelProvider: null, modelId: null, thinkingLevel: "high" } },
|
|
expected: {
|
|
provider: "space-provider",
|
|
id: "space-model",
|
|
credential: spaceCredential,
|
|
thinkingLevel: "high",
|
|
},
|
|
},
|
|
{
|
|
name: "does not select an incomplete bot override",
|
|
input: { bot: { ...bot, modelId: null }, overrideCredential },
|
|
expected: {
|
|
provider: "space-provider",
|
|
id: "space-model",
|
|
credential: spaceCredential,
|
|
thinkingLevel: "high",
|
|
},
|
|
},
|
|
{
|
|
name: "runs a mapped tier ahead of the bot's own model",
|
|
input: { bot, overrideCredential, tierModel },
|
|
expected: {
|
|
provider: "openai-compatible",
|
|
id: "router-code",
|
|
credential: routerCredential,
|
|
thinkingLevel: "high",
|
|
},
|
|
},
|
|
{
|
|
name: "runs a mapped tier ahead of the Space default",
|
|
input: { bot: { modelProvider: null, modelId: null, thinkingLevel: null }, tierModel },
|
|
expected: {
|
|
provider: "openai-compatible",
|
|
id: "router-code",
|
|
credential: routerCredential,
|
|
thinkingLevel: null,
|
|
},
|
|
},
|
|
{
|
|
name: "keeps the bot's own model when its tier is unmapped",
|
|
input: { bot, overrideCredential, tierModel: null },
|
|
expected: {
|
|
provider: "bot-provider",
|
|
id: "bot-model",
|
|
credential: overrideCredential,
|
|
thinkingLevel: "high",
|
|
},
|
|
},
|
|
{
|
|
name: "keeps the Space default when the tier is unmapped and nothing is pinned",
|
|
input: { bot: { modelProvider: null, modelId: null, thinkingLevel: "low" }, tierModel: null },
|
|
expected: {
|
|
provider: "space-provider",
|
|
id: "space-model",
|
|
credential: spaceCredential,
|
|
thinkingLevel: "low",
|
|
},
|
|
},
|
|
{
|
|
name: "uses settings before deployment defaults without inventing a credential",
|
|
input: { defaultCredential: null },
|
|
expected: {
|
|
provider: "settings-provider",
|
|
id: "settings-model",
|
|
credential: null,
|
|
thinkingLevel: null,
|
|
},
|
|
},
|
|
{
|
|
name: "uses deployment defaults when no stored configuration exists",
|
|
input: { defaultCredential: null, settings: null },
|
|
expected: {
|
|
provider: "deployment-provider",
|
|
id: "deployment-model",
|
|
credential: null,
|
|
thinkingLevel: null,
|
|
},
|
|
},
|
|
{
|
|
name: "leaves missing configuration for the caller's runtime fallback or failure path",
|
|
input: { defaultCredential: null, settings: null, deployment: null },
|
|
expected: {
|
|
provider: undefined,
|
|
id: undefined,
|
|
credential: null,
|
|
thinkingLevel: null,
|
|
},
|
|
},
|
|
])("$name", ({ input, expected }) => {
|
|
expect(selectConfiguredModel({ ...defaults, ...input })).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("connected model validation", () => {
|
|
const actor: Pick<Actor, "userId" | "spaceId"> = {
|
|
userId: "user-1",
|
|
spaceId: "space-1",
|
|
};
|
|
|
|
it("accepts catalog and saved free-form models but rejects unavailable choices", async () => {
|
|
const catalogPrisma = {
|
|
spaceModelPreference: { findFirst: async () => null },
|
|
userModelCredential: { findFirst: async () => credential("xai", null) },
|
|
} as unknown as PrismaClient;
|
|
await expect(
|
|
validateConnectedModelChoice(catalogPrisma, actor, "xai", "grok-4.6"),
|
|
).resolves.toBeUndefined();
|
|
await expect(
|
|
validateConnectedModelChoice(catalogPrisma, actor, "xai", "not-a-model"),
|
|
).resolves.toBe("Unknown model for that provider");
|
|
|
|
const preferenceFindFirst = vi.fn(
|
|
async (args: {
|
|
where: {
|
|
spaceId?: string;
|
|
userId?: string;
|
|
modelId?: string;
|
|
credential?: { provider?: string; userId?: string };
|
|
};
|
|
}) => {
|
|
if (args.where.modelId) {
|
|
if (
|
|
args.where.spaceId === actor.spaceId &&
|
|
args.where.userId === actor.userId &&
|
|
args.where.modelId === "private-model" &&
|
|
args.where.credential?.provider === "openai-compatible" &&
|
|
args.where.credential?.userId === actor.userId
|
|
) {
|
|
return { id: "saved-private-model" };
|
|
}
|
|
return null;
|
|
}
|
|
if (args.where.credential?.provider === "openai-compatible") {
|
|
return {
|
|
credential: credential("openai-compatible", "newest-model"),
|
|
isDefault: true,
|
|
modelId: "newest-model",
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
const customPrisma = {
|
|
spaceModelPreference: { findFirst: preferenceFindFirst },
|
|
userModelCredential: { findFirst: async () => null },
|
|
} as unknown as PrismaClient;
|
|
await expect(
|
|
validateConnectedModelChoice(customPrisma, actor, "openai-compatible", "private-model"),
|
|
).resolves.toBeUndefined();
|
|
expect(preferenceFindFirst).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
spaceId: actor.spaceId,
|
|
userId: actor.userId,
|
|
modelId: "private-model",
|
|
credential: { userId: actor.userId, provider: "openai-compatible" },
|
|
}),
|
|
select: { id: true },
|
|
}),
|
|
);
|
|
await expect(
|
|
validateConnectedModelChoice(customPrisma, actor, "openai-compatible", "missing-model"),
|
|
).resolves.toBe("Unknown model for that provider");
|
|
|
|
const disconnectedPrisma = {
|
|
spaceModelPreference: { findFirst: async () => null },
|
|
userModelCredential: { findFirst: async () => null },
|
|
} as unknown as PrismaClient;
|
|
await expect(
|
|
validateConnectedModelChoice(disconnectedPrisma, actor, "anthropic", "claude-opus-4-6"),
|
|
).resolves.toBe("Connect that model provider first");
|
|
});
|
|
});
|