refactor(ui): simplify Auto Setup model selection

This commit is contained in:
moe-berri 2026-09-03 19:28:54 -07:00
parent a2e5e7e066
commit 8acb8de997
5 changed files with 112 additions and 314 deletions

View file

@ -90,9 +90,6 @@ export interface AutoRouterCandidateDeployment {
export interface AutoRouterDeployment extends AutoRouterCandidateDeployment {
litellm_params?: {
model?: string | null;
base_model?: string | null;
input_cost_per_token?: number | null;
output_cost_per_token?: number | null;
complexity_router_config?: unknown;
complexity_router_default_model?: string | null;
auto_router_config?: unknown;
@ -108,8 +105,6 @@ export interface AutoRouterDeployment extends AutoRouterCandidateDeployment {
/** False for config.yaml-defined deployments, which the update and delete routes refuse. */
db_model?: boolean | null;
base_model?: string | null;
input_cost_per_token?: number | null;
output_cost_per_token?: number | null;
created_at?: string | null;
updated_at?: string | null;
team_id?: string | null;

View file

@ -19,10 +19,6 @@ vi.mock(
"@/app/(dashboard)/hooks/autoRouter/useAutoRouterPresets",
async () => await import("../../../tests/mocks/autoRouterPresets"),
);
vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({
useModelCostMap: () => ({ data: {}, isLoading: false }),
}));
const getAllPresets = (): AutoRouterPreset[] => BUNDLED_PRESETS;
const getPresetByKey = (key: string): AutoRouterPreset | undefined => BUNDLED_PRESETS.find((p) => p.key === key);
@ -158,56 +154,38 @@ describe("AddAutoRouterTab", () => {
expect(screen.getByText("Complexity Tier Configuration")).toBeInTheDocument();
});
it("configures four unique tiers from the available models with one click", async () => {
it("hides automatic setup when no available model is recommended", async () => {
mockFetchAvailableModels.mockResolvedValue([
{ model_group: "premium", mode: "chat" },
{ model_group: "cheap", mode: "chat" },
{ model_group: "best", mode: "chat" },
{ model_group: "middle", mode: "chat" },
]);
mockFetchAllModelDeployments.mockResolvedValue([
{ model_name: "cheap", litellm_params: { model: "cheap", input_cost_per_token: 1 } },
{ model_name: "middle", litellm_params: { model: "middle", input_cost_per_token: 2 } },
{ model_name: "premium", litellm_params: { model: "premium", input_cost_per_token: 3 } },
{ model_name: "best", litellm_params: { model: "best", input_cost_per_token: 4 } },
{ model_group: "unknown-model-a", mode: "chat" },
{ model_group: "unknown-model-b", mode: "chat" },
]);
renderWithProviders(<Harness />);
const button = screen.getByTestId("configure-automatically-button");
await waitFor(() => expect(button).toBeEnabled());
await userEvent.click(button);
expect(screen.getByText(/Simple: cheap.*Medium: middle.*Complex: premium.*Reasoning: best/)).toBeInTheDocument();
openTemplateDropdown();
await waitFor(() => expect(optionByLabel("Anthropic Family")).toHaveTextContent("Missing:"));
expect(screen.queryByTestId("configure-automatically-button")).not.toBeInTheDocument();
});
it("prefers the first compatible bundled template over the price fallback", async () => {
const firstPreset = getAllPresets()[0];
it("mixes preferred tier models even when one complete preset is available", async () => {
const anthropicPreset = getPresetByKey("anthropic_family")!;
mockFetchAvailableModels.mockResolvedValue(
[...getRequiredModelsInPreset(firstPreset)].map((model_group) => ({ model_group, mode: "chat" })),
[...getRequiredModelsInPreset(anthropicPreset), "gpt-5.6-luna"].map((model_group) => ({
model_group,
mode: "chat",
})),
);
mockFetchAllModelDeployments.mockResolvedValue([]);
renderWithProviders(<Harness />);
const button = screen.getByTestId("configure-automatically-button");
await waitFor(() => expect(button).toBeEnabled());
const button = await screen.findByTestId("configure-automatically-button");
await userEvent.click(button);
expect(toast.success).toHaveBeenCalledWith(`Configured with ${firstPreset.label}`);
});
it("prefers the OpenAI template over Gemini", async () => {
const openAiPreset = getPresetByKey("openai_family")!;
const geminiPreset = getPresetByKey("gemini_family")!;
const available = new Set([...getRequiredModelsInPreset(openAiPreset), ...getRequiredModelsInPreset(geminiPreset)]);
mockFetchAvailableModels.mockResolvedValue([...available].map((model_group) => ({ model_group, mode: "chat" })));
mockFetchAllModelDeployments.mockResolvedValue([]);
renderWithProviders(<Harness />);
const button = screen.getByTestId("configure-automatically-button");
await waitFor(() => expect(button).toBeEnabled());
await userEvent.click(button);
expect(toast.success).toHaveBeenCalledWith(`Configured with ${openAiPreset.label}`);
expect(
screen.getByText(
/Simple: gpt-5.6-luna.*Medium: claude-sonnet-5.*Complex: claude-opus-5.*Reasoning: claude-opus-5/,
),
).toBeInTheDocument();
expect(toast.success).not.toHaveBeenCalledWith(expect.stringContaining("Configured with"));
});
it("mixes available models from the preferred tier catalog when no complete template fits", async () => {
@ -220,8 +198,7 @@ describe("AddAutoRouterTab", () => {
mockFetchAllModelDeployments.mockResolvedValue([]);
renderWithProviders(<Harness />);
const button = screen.getByTestId("configure-automatically-button");
await waitFor(() => expect(button).toBeEnabled());
const button = await screen.findByTestId("configure-automatically-button");
await userEvent.click(button);
expect(

View file

@ -21,7 +21,6 @@ import TeamDropdown from "../common_components/team_dropdown";
import { type AddAutoRouterValues, handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
import { fetchAvailableModels, type ModelGroup } from "@/components/llm_calls/fetch_models";
import { autoRouterListKey, fetchAllModelDeployments } from "@/app/(dashboard)/hooks/models/useModels";
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import ComplexityRouterConfig, {
ComplexityRouterConfigValue,
effectiveClassifierType,
@ -106,7 +105,6 @@ const presetDisabledHint = (availability: PresetAvailability): string | null =>
const isPresetHintAlarming = (availability: PresetAvailability): boolean => availability.kind === "missing_models";
const NO_PRESETS: AutoRouterPreset[] = [];
const AUTO_SETUP_PRESET_PRIORITY = ["1m_context", "anthropic_family", "openai_family", "gemini_family", "lite"];
// A one-line summary of what's configured, shown when the detailed section is collapsed so a
// caller can see the shape of the config without opening it.
@ -236,7 +234,6 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
queryFn: () => fetchAllModelDeployments(accessToken, userId ?? "", userRole),
enabled: Boolean(accessToken),
});
const { data: modelCostMap = {}, isLoading: costsLoading } = useModelCostMap();
const modelsLoading = groupsLoading || deploymentsLoading;
const modelInfo = React.useMemo(() => data ?? [], [data]);
const {
@ -246,7 +243,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
refetch: refetchPresets,
} = useAutoRouterPresets();
const presets = presetsData ?? NO_PRESETS;
const automaticSetupLoading = modelsLoading || costsLoading || presetsPending;
const automaticSetupLoading = modelsLoading || presetsPending;
const presetsUnavailable = presetsError && presetsData === undefined;
// react-query keeps the last successful list around when a later refetch fails, so isError alone
// can't tell "never loaded" apart from "loaded, then a background refetch errored" - only the
@ -271,6 +268,14 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
),
[modelInfo],
);
const preferredTierModels = React.useMemo(
() => buildPreferredTierModels(presets, availability),
[presets, availability],
);
const automaticRouterConfig = React.useMemo(
() => buildAutomaticRouterConfig(modelInfo, deployments ?? [], preferredTierModels),
[modelInfo, deployments, preferredTierModels],
);
// A preset's models can only be trusted against a successfully loaded list. Selection and the
// greyed-out state derive from this one function, so a preset that cannot be selected can never
@ -319,30 +324,11 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
};
const handleAutomaticSetup = () => {
const prioritizedPresets = AUTO_SETUP_PRESET_PRIORITY.flatMap((key) => {
const preset = presets.find((candidate) => candidate.key === key);
return preset ? [preset] : [];
});
const matchingPreset = prioritizedPresets.find((preset) => presetAvailability(preset).kind === "available");
if (matchingPreset) {
const presetState = presetAvailability(matchingPreset);
setSelectedPreset(matchingPreset.key);
applyPrefill(buildPresetPrefill(matchingPreset.complexity_router_config, availability));
setDetailsExpanded(presetState.kind === "available" && presetState.viaDeployments);
toast.success(`Configured with ${matchingPreset.label}`);
return;
}
const preferredTierModels = buildPreferredTierModels(prioritizedPresets, availability);
const generatedConfig = buildAutomaticRouterConfig(modelInfo, deployments ?? [], modelCostMap, preferredTierModels);
if (generatedConfig === null) {
toast.fromError("Add at least one chat model before configuring an Auto Router");
return;
}
if (automaticRouterConfig === null) return;
setSelectedPreset(undefined);
applyPrefill({ ...buildEmptyPrefill(), complexityRouterConfig: generatedConfig });
applyPrefill({ ...buildEmptyPrefill(), complexityRouterConfig: automaticRouterConfig });
setDetailsExpanded(false);
toast.success("Automatic setup created", { description: tierConfigSummary(generatedConfig) });
toast.success("Automatic setup created", { description: tierConfigSummary(automaticRouterConfig) });
};
const handlePresetChange = (presetKey: string | undefined) => {
@ -540,24 +526,20 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="e.g., smart_router, auto_router_1" />}
</FormField>
<div className="flex items-center justify-between gap-4 rounded-lg border border-border p-4">
<div>
<div className="text-sm font-medium text-foreground">Start with a recommended setup</div>
<div className="text-xs text-muted-foreground">
Uses your available models and our recommended setups. You can review and edit everything before
saving.
{!automaticSetupLoading && automaticRouterConfig && (
<div className="flex items-center justify-between gap-4 rounded-lg border border-border p-4">
<div>
<div className="text-sm font-medium text-foreground">Start with a recommended setup</div>
<div className="text-xs text-muted-foreground">
Uses your available models and our recommended setups. You can review and edit everything before
saving.
</div>
</div>
<Button type="button" data-testid="configure-automatically-button" onClick={handleAutomaticSetup}>
Configure automatically
</Button>
</div>
<Button
type="button"
data-testid="configure-automatically-button"
disabled={automaticSetupLoading}
onClick={handleAutomaticSetup}
>
{automaticSetupLoading && <UiLoadingSpinner className="size-4" />}
Configure automatically
</Button>
</div>
)}
<div>
<label className="block text-sm font-medium text-foreground mb-2">Template</label>

View file

@ -1,184 +1,89 @@
import { describe, expect, it } from "vitest";
import type { AutoRouterDeployment } from "@/app/(dashboard)/hooks/models/useModels";
import { buildAutomaticRouterConfig, type PreferredTierModels } from "./auto_setup";
import { buildModelAvailability } from "@/lib/autorouter_presets";
import { buildAutomaticRouterConfig, buildPreferredTierModels, type PreferredTierModels } from "./auto_setup";
const models = (...names: string[]) => names.map((model_group) => ({ model_group, mode: "chat" }));
const deployment = (model_name: string, model = model_name): AutoRouterDeployment => ({
model_name,
litellm_params: { model },
});
const tierModels = (config: ReturnType<typeof buildAutomaticRouterConfig>) =>
config && Object.values(config.tiers).map((tier) => (typeof tier === "string" ? tier : tier[0]));
const deployment = (name: string, cost: number): AutoRouterDeployment => ({
model_name: name,
litellm_params: {
model: name,
input_cost_per_token: cost / 2,
output_cost_per_token: cost / 2,
},
describe("buildPreferredTierModels", () => {
it("recognizes curated models that are not in a preset", () => {
const available = ["gpt-4o-mini", "claude-sonnet-4-5", "grok-4", "deepseek-reasoner"];
const availability = buildModelAvailability(available, []);
const preferred = buildPreferredTierModels([], availability);
expect(preferred).toEqual({
SIMPLE: ["gpt-4o-mini"],
MEDIUM: ["claude-sonnet-4-5"],
COMPLEX: ["grok-4"],
REASONING: ["deepseek-reasoner"],
});
});
});
const firstModel = (value: string | string[]): string => (typeof value === "string" ? value : value[0]);
const tierModels = (config: ReturnType<typeof buildAutomaticRouterConfig>) =>
config && [
firstModel(config.tiers.SIMPLE),
firstModel(config.tiers.MEDIUM),
firstModel(config.tiers.COMPLEX),
firstModel(config.tiers.REASONING),
];
describe("buildAutomaticRouterConfig", () => {
it("uses available preferred models before price ranking", () => {
it("selects one preferred model for each tier", () => {
const preferred: PreferredTierModels = {
SIMPLE: ["preferred-simple"],
MEDIUM: ["preferred-medium"],
COMPLEX: ["preferred-complex"],
REASONING: ["preferred-reasoning"],
SIMPLE: ["simple"],
MEDIUM: ["medium"],
COMPLEX: ["complex"],
REASONING: ["reasoning"],
};
const available = [...Object.values(preferred).flat(), "cheap-decoy", "expensive-decoy"];
const config = buildAutomaticRouterConfig(
models(...available),
available.map((name, index) => deployment(name, index + 1)),
{},
preferred,
);
expect(tierModels(config)).toEqual([
"preferred-simple",
"preferred-medium",
"preferred-complex",
"preferred-reasoning",
]);
expect(
tierModels(buildAutomaticRouterConfig(models("simple", "medium", "complex", "reasoning"), [], preferred)),
).toEqual(["simple", "medium", "complex", "reasoning"]);
});
it("reuses the nearest preferred model for tiers with no preferred match", () => {
it("reuses the closest available tier when a tier has no match", () => {
const preferred: PreferredTierModels = {
SIMPLE: ["preferred-simple"],
SIMPLE: ["simple"],
MEDIUM: [],
COMPLEX: ["preferred-complex"],
COMPLEX: ["complex"],
REASONING: [],
};
const config = buildAutomaticRouterConfig(
models("preferred-simple", "preferred-complex", "cheap-decoy"),
[deployment("preferred-simple", 4), deployment("preferred-complex", 5), deployment("cheap-decoy", 1)],
{},
preferred,
);
expect(tierModels(config)).toEqual([
"preferred-simple",
"preferred-simple",
"preferred-complex",
"preferred-complex",
expect(tierModels(buildAutomaticRouterConfig(models("simple", "complex"), [], preferred))).toEqual([
"simple",
"simple",
"complex",
"complex",
]);
});
it("uses price ranking when none of the preferred models are available", () => {
const unavailablePreferred: PreferredTierModels = {
it("returns null when none of the available models are recommended", () => {
const preferred: PreferredTierModels = {
SIMPLE: ["missing-simple"],
MEDIUM: ["missing-medium"],
COMPLEX: ["missing-complex"],
REASONING: ["missing-reasoning"],
};
const config = buildAutomaticRouterConfig(
models("expensive", "cheap", "premium", "middle"),
[deployment("cheap", 1), deployment("middle", 2), deployment("premium", 3), deployment("expensive", 4)],
{},
unavailablePreferred,
);
expect(tierModels(config)).toEqual(["cheap", "middle", "premium", "expensive"]);
expect(buildAutomaticRouterConfig(models("unknown-model"), [], preferred)).toBeNull();
});
it("uses four different models when four are available", () => {
const config = buildAutomaticRouterConfig(
models("expensive", "cheap", "premium", "middle"),
[deployment("cheap", 1), deployment("middle", 2), deployment("premium", 3), deployment("expensive", 4)],
{},
);
expect(tierModels(config)).toEqual(["cheap", "middle", "premium", "expensive"]);
expect(config?.classifier_type).toBe("heuristic_v2");
});
it("selects one model per tier from a large inventory", () => {
const names = Array.from({ length: 100 }, (_, index) => `model-${index.toString().padStart(3, "0")}`);
const config = buildAutomaticRouterConfig(
models(...names),
names.map((name, index) => deployment(name, index + 1)),
{},
);
const expectedTiers = {
SIMPLE: ["model-000"],
MEDIUM: ["model-033"],
COMPLEX: ["model-066"],
REASONING: ["model-099"],
it("ignores non-chat models and existing auto routers", () => {
const preferred: PreferredTierModels = {
SIMPLE: ["gpt-4o-mini", "smart-router"],
MEDIUM: [],
COMPLEX: [],
REASONING: [],
};
const available = [
{ model_group: "gpt-4o-mini", mode: "chat" },
{ model_group: "image-model", mode: "image_generation" },
{ model_group: "smart-router", mode: "chat" },
];
expect(config?.tiers).toEqual(expectedTiers);
});
it("only repeats models when fewer than four are available", () => {
expect(
tierModels(
buildAutomaticRouterConfig(
models("cheap", "expensive"),
[deployment("cheap", 1), deployment("expensive", 4)],
{},
),
buildAutomaticRouterConfig(available, [deployment("smart-router", "auto_router/complexity_router")], preferred),
),
).toEqual(["cheap", "cheap", "expensive", "expensive"]);
});
it("uses the published cost map when deployments do not define prices", () => {
const config = buildAutomaticRouterConfig(
models("premium", "cheap", "middle"),
[
{ model_name: "premium", litellm_params: { model: "provider/premium" } },
{ model_name: "cheap", litellm_params: { model: "provider/cheap" } },
{ model_name: "middle", litellm_params: { model: "provider/middle" } },
],
{
"provider/cheap": { input_cost_per_token: 1, output_cost_per_token: 1 },
"provider/middle": { input_cost_per_token: 2, output_cost_per_token: 2 },
"provider/premium": { input_cost_per_token: 3, output_cost_per_token: 3 },
},
);
expect(tierModels(config)).toEqual(["cheap", "middle", "premium", "premium"]);
});
it("uses the most expensive deployment when a group has several", () => {
const config = buildAutomaticRouterConfig(
models("variable", "steady", "premium", "top"),
[
deployment("variable", 1),
deployment("variable", 8),
deployment("steady", 2),
deployment("premium", 3),
deployment("top", 4),
],
{},
);
expect(tierModels(config)).toEqual(["steady", "premium", "top", "variable"]);
});
it("ignores non-chat and existing auto-router models", () => {
const config = buildAutomaticRouterConfig(
[
{ model_group: "chat-model", mode: "chat" },
{ model_group: "image-model", mode: "image_generation" },
{ model_group: "auto_router/existing", mode: "chat" },
{ model_group: "smart-router", mode: "chat" },
],
[
deployment("chat-model", 1),
{ model_name: "smart-router", litellm_params: { model: "auto_router/complexity_router" } },
],
{},
);
expect(tierModels(config)).toEqual(["chat-model", "chat-model", "chat-model", "chat-model"]);
});
it("returns null when there are no usable models", () => {
expect(buildAutomaticRouterConfig([], [], {})).toBeNull();
).toEqual(["gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini"]);
});
});

View file

@ -3,62 +3,15 @@ import type { ModelGroup } from "@/components/llm_calls/fetch_models";
import { resolveAvailableModel, type AutoRouterPreset, type ModelAvailability } from "@/lib/autorouter_presets";
import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
type ModelCost = {
input_cost_per_token?: number | null;
output_cost_per_token?: number | null;
};
export type ModelCostMap = Record<string, ModelCost>;
const TIER_NAMES = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"] as const;
type TierName = (typeof TIER_NAMES)[number];
export type PreferredTierModels = Record<TierName, string[]>;
const price = (cost: ModelCost | null | undefined): number | undefined => {
const input = cost?.input_cost_per_token;
const output = cost?.output_cost_per_token;
if (typeof input !== "number" && typeof output !== "number") return undefined;
return (input ?? 0) + (output ?? 0);
};
const deploymentPrice = (deployment: AutoRouterDeployment, costMap: ModelCostMap): number | undefined => {
const configured = price(deployment.litellm_params) ?? price(deployment.model_info);
if (configured !== undefined) return configured;
const references = [
deployment.litellm_params?.model,
deployment.litellm_params?.base_model,
deployment.model_info?.base_model,
];
for (const reference of references) {
if (reference && costMap[reference]) return price(costMap[reference]);
}
return undefined;
};
const groupPrice = (
modelGroup: string,
deployments: AutoRouterDeployment[],
costMap: ModelCostMap,
): number | undefined => {
const groupDeployments = deployments.filter(
(deployment) =>
deployment.model_name === modelGroup && !deployment.litellm_params?.model?.startsWith("auto_router/"),
);
if (groupDeployments.length === 0) return price(costMap[modelGroup]);
const prices = groupDeployments.map((deployment) => deploymentPrice(deployment, costMap));
if (prices.some((value) => value === undefined)) return undefined;
const knownPrices = prices.filter((value): value is number => value !== undefined);
return Math.max(...knownPrices);
};
const selectTierModels = (ranked: string[]): [string, string, string, string] => {
if (ranked.length === 1) return [ranked[0], ranked[0], ranked[0], ranked[0]];
if (ranked.length === 2) return [ranked[0], ranked[0], ranked[1], ranked[1]];
if (ranked.length === 3) return [ranked[0], ranked[1], ranked[2], ranked[2]];
const last = ranked.length - 1;
return [ranked[0], ranked[Math.floor(last / 3)], ranked[Math.floor((2 * last) / 3)], ranked[last]];
const ADDITIONAL_TIER_MODELS: PreferredTierModels = {
SIMPLE: ["gpt-4o-mini", "gpt-5-mini", "gemini-2.5-flash", "deepseek-chat"],
MEDIUM: ["gpt-5-mini", "gpt-4o", "claude-sonnet-4-5", "gemini-2.5-flash", "deepseek-chat"],
COMPLEX: ["gpt-5", "gpt-4o", "claude-sonnet-4-6", "gemini-2.5-pro", "grok-4"],
REASONING: ["o3", "deepseek-reasoner", "claude-opus-4-6", "gemini-2.5-pro", "gpt-5"],
};
export const buildPreferredTierModels = (
@ -70,12 +23,13 @@ export const buildPreferredTierModels = (
tier,
Array.from(
new Set(
presets.flatMap((preset) =>
preset.complexity_router_config.tiers[tier].flatMap((model) => {
const resolved = resolveAvailableModel(model, availability);
return resolved ? [resolved] : [];
}),
),
[
...presets.flatMap((preset) => preset.complexity_router_config.tiers[tier]),
...ADDITIONAL_TIER_MODELS[tier],
].flatMap((model) => {
const resolved = resolveAvailableModel(model, availability);
return resolved ? [resolved] : [];
}),
),
),
]),
@ -99,8 +53,7 @@ const selectPreferredTierModels = (
export const buildAutomaticRouterConfig = (
models: ModelGroup[],
deployments: AutoRouterDeployment[],
costMap: ModelCostMap,
preferredByTier?: PreferredTierModels,
preferredByTier: PreferredTierModels,
): ComplexityRouterConfigValue | null => {
const autoRouterNames: ReadonlySet<string> = new Set(
deployments
@ -117,22 +70,8 @@ export const buildAutomaticRouterConfig = (
);
if (names.length === 0) return null;
const usableNames: ReadonlySet<string> = new Set(names);
const ranked = names
.map((name) => ({ name, price: groupPrice(name, deployments, costMap) }))
.sort((left, right) => {
if (left.price === undefined && right.price !== undefined) return 1;
if (left.price !== undefined && right.price === undefined) return -1;
if (left.price !== undefined && right.price !== undefined && left.price !== right.price) {
return left.price - right.price;
}
return left.name.localeCompare(right.name);
})
.map(({ name }) => name);
const selected = preferredByTier
? selectPreferredTierModels(preferredByTier, usableNames) ?? selectTierModels(ranked)
: selectTierModels(ranked);
const selected = selectPreferredTierModels(preferredByTier, usableNames);
if (selected === null) return null;
return {
tiers: {