Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,53 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
]);
});

it("retains discovered models but never removed custom models", () => {
const discoveredModel = {
slug: "gpt-5.6-sol",
name: "GPT-5.6-Sol",
isCustom: false,
capabilities: null,
} as const;
const previousProvider = {
instanceId: ProviderInstanceId.make("codex"),
driver: ProviderDriverKind.make("codex"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-08-02T10:00:00.000Z",
version: "0.146.0",
models: [
discoveredModel,
{
slug: "removed-custom-model",
name: "removed-custom-model",
isCustom: true,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const successfulRefresh = {
...previousProvider,
checkedAt: "2026-08-02T10:01:00.000Z",
models: [discoveredModel],
} satisfies ServerProvider;
const incompleteRefresh = {
...previousProvider,
checkedAt: "2026-08-02T10:02:00.000Z",
models: [],
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, successfulRefresh).models, [
discoveredModel,
]);
assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, incompleteRefresh).models, [
discoveredModel,
]);
});

it("drops stale OpenCode models missing from a successful refresh", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
Expand Down
7 changes: 4 additions & 3 deletions apps/server/src/provider/Layers/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ const mergeProviderModels = (
nextModels: ReadonlyArray<ServerProvider["models"][number]>,
): ReadonlyArray<ServerProvider["models"][number]> => {
const shouldRetainMissingModels = shouldRetainMissingProviderModels(provider);
const retainablePreviousModels = previousModels.filter((model) => !model.isCustom);

if (shouldRetainMissingModels && nextModels.length === 0 && previousModels.length > 0) {
return previousModels;
if (shouldRetainMissingModels && nextModels.length === 0 && retainablePreviousModels.length > 0) {
return retainablePreviousModels;
}

const previousBySlug = new Map(previousModels.map((model) => [model.slug, model] as const));
Expand All @@ -119,7 +120,7 @@ const mergeProviderModels = (
});
const nextSlugs = new Set(nextModels.map((model) => model.slug));
return shouldRetainMissingModels
? [...mergedModels, ...previousModels.filter((model) => !nextSlugs.has(model.slug))]
? [...mergedModels, ...retainablePreviousModels.filter((model) => !nextSlugs.has(model.slug))]
: mergedModels;
};

Expand Down
45 changes: 45 additions & 0 deletions apps/server/src/provider/providerStatusCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,51 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => {
);
});

it("uses current settings as the authority for cached custom models", () => {
const configuredCustomModel = {
slug: "configured-custom-model",
name: "configured-custom-model",
isCustom: true,
capabilities: emptyCapabilities,
} as const;
const cachedCodex = makeProvider(CODEX_DRIVER, {
models: [
configuredCustomModel,
{
slug: "removed-custom-model",
name: "removed-custom-model",
isCustom: true,
capabilities: emptyCapabilities,
},
{
slug: "gpt-5-mini",
name: "GPT-5 Mini",
isCustom: false,
capabilities: emptyCapabilities,
},
],
});
const fallbackCodex = makeProvider(CODEX_DRIVER, {
models: [configuredCustomModel],
});

assert.deepStrictEqual(
hydrateCachedProvider({
cachedProvider: cachedCodex,
fallbackProvider: fallbackCodex,
}).models,
[
configuredCustomModel,
{
slug: "gpt-5-mini",
name: "GPT-5 Mini",
isCustom: false,
capabilities: emptyCapabilities,
},
],
);
});

it("ignores stale cached enabled state when the provider is now disabled", () => {
const cachedCodex = makeProvider(CODEX_DRIVER, {
checkedAt: "2026-04-10T12:00:00.000Z",
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/provider/providerStatusCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ const mergeProviderModels = (
cachedModels: ReadonlyArray<ServerProvider["models"][number]>,
): ReadonlyArray<ServerProvider["models"][number]> => {
const fallbackSlugs = new Set(fallbackModels.map((model) => model.slug));
return [...fallbackModels, ...cachedModels.filter((model) => !fallbackSlugs.has(model.slug))];
return [
...fallbackModels,
...cachedModels.filter((model) => !model.isCustom && !fallbackSlugs.has(model.slug)),
];
};

export const orderProviderSnapshots = (
Expand Down
Loading