Skip to content
80 changes: 80 additions & 0 deletions apps/web/src/modelSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,86 @@ describe("instance-scoped model selection", () => {
).not.toContain("openai/gpt-5.5");
});

it("removes a custom model immediately when the provider snapshot is stale", () => {
const baseProvider = provider({
instanceId: "claudeAgent",
models: ["claude-sonnet-4-6"],
});
const staleProviders: ServerProvider[] = [
{
...baseProvider,
models: [
...baseProvider.models,
{
slug: "removed-preview-model",
name: "Removed Preview Model",
isCustom: true,
capabilities: {},
},
],
},
];
const settings: UnifiedSettings = {
...settingsWithProviderInstances(),
providerInstances: {
...settingsWithProviderInstances().providerInstances,
[ProviderInstanceId.make("claudeAgent")]: {
driver: ProviderDriverKind.make("claudeAgent"),
config: { customModels: [] },
},
},
};
const stock = deriveProviderInstanceEntries(staleProviders)[0]!;

expect(getAppModelOptionsForInstance(settings, stock).map((option) => option.slug)).toEqual([
"claude-sonnet-4-6",
]);
expect(
resolveAppModelSelectionForInstance(
ProviderInstanceId.make("claudeAgent"),
settings,
staleProviders,
"removed-preview-model",
),
).toBe("claude-sonnet-4-6");
});

it("does not fall back to a stale custom model when no configured models remain", () => {
const baseProvider = provider({ instanceId: "claudeAgent" });
const staleProviders: ServerProvider[] = [
{
...baseProvider,
models: [
{
slug: "removed-preview-model",
name: "Removed Preview Model",
isCustom: true,
capabilities: {},
},
],
},
];
const settings: UnifiedSettings = {
...settingsWithProviderInstances(),
providerInstances: {
...settingsWithProviderInstances().providerInstances,
[ProviderInstanceId.make("claudeAgent")]: {
driver: ProviderDriverKind.make("claudeAgent"),
config: { customModels: [] },
},
},
};

expect(
resolveAppModelSelectionForInstance(
ProviderInstanceId.make("claudeAgent"),
settings,
staleProviders,
"removed-preview-model",
),
).toBeNull();
});

it("hides server models from the instance option list", () => {
const providers = [
provider({
Expand Down
41 changes: 30 additions & 11 deletions apps/web/src/modelSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,16 @@ export function getAppModelOptions(
provider: ProviderDriverKind,
_selectedModel?: string | null,
): AppModelOption[] {
const options: AppModelOption[] = getProviderModels(providers, provider).map(toAppModelOption);
const liveModels = getProviderModels(providers, provider);
const options: AppModelOption[] = liveModels
.filter((model) => !model.isCustom)
.map(toAppModelOption);
const liveCustomModelsBySlug = new Map(
liveModels.filter((model) => model.isCustom).map((model) => [model.slug, model] as const),
);
const seen = new Set(options.map((option) => option.slug));
const builtInModelSlugs = new Set(
Arr.filterMap(getProviderModels(providers, provider), (model) =>
Arr.filterMap(liveModels, (model) =>
model.isCustom ? Result.failVoid : Result.succeed(model.slug),
),
);
Expand All @@ -172,11 +178,16 @@ export function getAppModelOptions(
}

seen.add(slug);
options.push({
slug,
name: slug,
isCustom: true,
});
const liveCustomModel = liveCustomModelsBySlug.get(slug);
options.push(
liveCustomModel
? toAppModelOption(liveCustomModel)
: {
slug,
name: slug,
isCustom: true,
},
);
}

return applyInstanceModelPreferences(
Expand All @@ -200,7 +211,12 @@ export function getAppModelOptionsForInstance(
settings: UnifiedSettings,
entry: ProviderInstanceEntry,
): AppModelOption[] {
const options: AppModelOption[] = entry.models.map(toAppModelOption);
const options: AppModelOption[] = entry.models
.filter((model) => !model.isCustom)
.map(toAppModelOption);
const liveCustomModelsBySlug = new Map(
entry.models.filter((model) => model.isCustom).map((model) => [model.slug, model] as const),
);
const seen = new Set(options.map((option) => option.slug));
const builtInModelSlugs = new Set(
Arr.filterMap(entry.models, (model) =>
Expand All @@ -215,7 +231,10 @@ export function getAppModelOptionsForInstance(
}

seen.add(slug);
options.push({ slug, name: slug, isCustom: true });
const liveCustomModel = liveCustomModelsBySlug.get(slug);
options.push(
liveCustomModel ? toAppModelOption(liveCustomModel) : { slug, name: slug, isCustom: true },
);
Comment thread
cursor[bot] marked this conversation as resolved.
}

return applyInstanceModelPreferences(
Expand Down Expand Up @@ -253,8 +272,8 @@ export function resolveAppModelSelectionForInstance(
resolveSelectableModel(entry.driverKind, selectedModel, options) ??
options.find((option) => option.isDefault)?.slug ??
options[0]?.slug ??
entry.models.find((model) => model.isDefault)?.slug ??
entry.models[0]?.slug ??
entry.models.find((model) => model.isDefault && !model.isCustom)?.slug ??
entry.models.find((model) => !model.isCustom)?.slug ??
null
);
}
Expand Down
Loading