evOo-local/packages/adapters/src/model-selection.test.ts
Claude f28f9b7d55
feat(models): map model tiers per space and let bots and helpers ask for one
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
2026-10-02 22:07:32 +00:00

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");
});
});