authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 13:24:40-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 13:24:40-07:00
logbfef973d9ec64e580fff4197ac53085207471ffd
treefe3e26c45ef1cff6b1e6a10b6119112660c8d729
parent163d86a78430d23414abde323ca70d8379280c0b
signature Signed by PGP key B5690EEEBB952194

fix(server): remove retired Codex models after refresh (#9773)


2 files changed, 189 insertions(+), 4 deletions(-)

apps/server/src/provider/Layers/ProviderRegistry.test.ts+182-1
...@@ -49,7 +49,11 @@ import {...@@ -49,7 +49,11 @@ import {
49} from "./ProviderRegistry.ts";49} from "./ProviderRegistry.ts";
50import * as ServerConfig from "../../config.ts";50import * as ServerConfig from "../../config.ts";
51import * as ServerSettingsModule from "../../serverSettings.ts";51import * as ServerSettingsModule from "../../serverSettings.ts";
52import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";52import {
53 readProviderStatusCache,
54 resolveProviderStatusCachePath,
55 writeProviderStatusCache,
56} from "../providerStatusCache.ts";
53import { COMPACT_SLASH_COMMAND } from "../providerSnapshot.ts";57import { COMPACT_SLASH_COMMAND } from "../providerSnapshot.ts";
54import type { ProviderInstance } from "../ProviderDriver.ts";58import type { ProviderInstance } from "../ProviderDriver.ts";
55import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";59import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
...@@ -900,6 +904,183 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -900,6 +904,183 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
900 assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);904 assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);
901 });905 });
902906
907 describe("Codex model inventories", () => {
908 const cachedProvider = {
909 instanceId: ProviderInstanceId.make("codex-personal"),
910 driver: ProviderDriverKind.make("codex"),
911 status: "ready",
912 enabled: true,
913 installed: true,
914 auth: { status: "authenticated" },
915 checkedAt: "2026-09-04T19:00:00.000Z",
916 version: "0.153.3",
917 models: [
918 "vega-alpha",
919 "joule-alpha",
920 "kindle-alpha",
921 "ultima-alpha",
922 "solstice-alpha",
923 ].map((slug) => ({ slug, name: slug, isCustom: false, capabilities: null })),
924 slashCommands: [],
925 skills: [],
926 } satisfies ServerProvider;
927 const customModel = {
928 slug: "custom-model",
929 name: "Custom model",
930 isCustom: true,
931 capabilities: null,
932 } as const;
933 const refreshedProvider = {
934 ...cachedProvider,
935 checkedAt: "2026-09-04T19:01:00.000Z",
936 models: [
937 { slug: "gpt-6-astra", name: "GPT 6 Astra", isCustom: false, capabilities: null },
938 cachedProvider.models[0]!,
939 customModel,
940 ],
941 } satisfies ServerProvider;
942 const pendingProvider = {
943 ...cachedProvider,
944 status: "warning",
945 installed: false,
946 auth: { status: "unknown" },
947 models: [customModel],
948 } satisfies ServerProvider;
949 const failedProvider = {
950 ...pendingProvider,
951 checkedAt: "2026-09-04T19:02:00.000Z",
952 status: "error",
953 installed: true,
954 } satisfies ServerProvider;
955
956 it("drops retired alpha models after discovery, including without OpenAI authentication", () => {
957 for (const authStatus of ["authenticated", "unknown"] as const) {
958 assert.deepStrictEqual(
959 mergeProviderSnapshot(cachedProvider, {
960 ...refreshedProvider,
961 auth: { status: authStatus },
962 }).models,
963 refreshedProvider.models,
964 );
965 }
966 });
967
968 it("keeps discovered models during startup and failed probes without restoring removed custom models", () => {
969 for (const provider of [pendingProvider, failedProvider]) {
970 assert.deepStrictEqual(
971 mergeProviderSnapshot(
972 {
973 ...cachedProvider,
974 models: [...cachedProvider.models, { ...customModel, slug: "removed-custom" }],
975 },
976 provider,
977 ).models,
978 [customModel, ...cachedProvider.models],
979 );
980 }
981 });
982
983 it("clears discovered models after sign-out, disable, uninstall, or empty discovery", () => {
984 const emptyProvider = { ...refreshedProvider, models: [customModel] };
985 const clearedProviders = [
986 { ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
987 { ...emptyProvider, status: "disabled", enabled: false },
988 { ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
989 emptyProvider,
990 { ...emptyProvider, models: [] },
991 ] satisfies ReadonlyArray<ServerProvider>;
992
993 for (const provider of clearedProviders) {
994 assert.deepStrictEqual(
995 mergeProviderSnapshot(cachedProvider, provider).models,
996 provider.models,
997 );
998 }
999 });
1000
1001 it.effect("persists removals across failed refreshes and registry restarts", () =>
1002 Effect.gen(function* () {
1003 const config = yield* ServerConfig.ServerConfig;
1004 const filePath = yield* resolveProviderStatusCachePath({
1005 cacheDir: config.providerStatusCacheDir,
1006 instanceId: cachedProvider.instanceId,
1007 });
1008 yield* writeProviderStatusCache({ filePath, provider: cachedProvider });
1009 const nextProvider = yield* Ref.make<ServerProvider>(refreshedProvider);
1010 const instance = {
1011 instanceId: cachedProvider.instanceId,
1012 driverKind: cachedProvider.driver,
1013 continuationIdentity: {
1014 driverKind: cachedProvider.driver,
1015 continuationKey: "codex:instance:codex-personal",
1016 },
1017 displayName: undefined,
1018 enabled: true,
1019 snapshot: {
1020 maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({
1021 provider: cachedProvider.driver,
1022 packageName: null,
1023 }),
1024 getSnapshot: Effect.succeed(pendingProvider),
1025 refresh: Ref.get(nextProvider),
1026 streamChanges: Stream.empty,
1027 applyUsageLimits: () => Effect.void,
1028 },
1029 adapter: {} as ProviderInstance["adapter"],
1030 textGeneration: {} as ProviderInstance["textGeneration"],
1031 } satisfies ProviderInstance;
1032 const instanceRegistryLayer = Layer.succeed(
1033 ProviderInstanceRegistry.ProviderInstanceRegistry,
1034 {
1035 getInstance: (id) =>
1036 Effect.succeed(id === instance.instanceId ? instance : undefined),
1037 listInstances: Effect.succeed([instance]),
1038 listUnavailable: Effect.succeed([]),
1039 streamChanges: Stream.empty,
1040 subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
1041 },
1042 );
1043 const retainedModels = [
1044 customModel,
1045 ...refreshedProvider.models.filter((model) => !model.isCustom),
1046 ];
1047
1048 for (const restarted of [false, true]) {
1049 yield* Effect.gen(function* () {
1050 const registry = yield* ProviderRegistry.ProviderRegistry;
1051 const expectedModels = restarted
1052 ? retainedModels
1053 : [customModel, ...cachedProvider.models];
1054 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, expectedModels);
1055
1056 yield* registry.refreshInstance(instance.instanceId);
1057 assert.deepStrictEqual(
1058 (yield* readProviderStatusCache(filePath))?.models,
1059 restarted ? retainedModels : refreshedProvider.models,
1060 );
1061
1062 yield* Ref.set(nextProvider, failedProvider);
1063 const afterFailure = yield* registry.refreshInstance(instance.instanceId);
1064 assert.deepStrictEqual(afterFailure[0]?.models, retainedModels);
1065 assert.deepStrictEqual(
1066 (yield* readProviderStatusCache(filePath))?.models,
1067 retainedModels,
1068 );
1069 }).pipe(
1070 Effect.provide(ProviderRegistryLive.pipe(Layer.provide(instanceRegistryLayer))),
1071 Effect.scoped,
1072 );
1073 }
1074 }).pipe(
1075 Effect.provide(
1076 ServerConfig.layerTest(process.cwd(), {
1077 prefix: "t3-codex-retired-model-cache-",
1078 }).pipe(Layer.provideMerge(NodeServices.layer)),
1079 ),
1080 ),
1081 );
1082 });
1083
903 describe("Antigravity model inventories", () => {1084 describe("Antigravity model inventories", () => {
904 const previousProvider = {1085 const previousProvider = {
905 instanceId: ProviderInstanceId.make("antigravity-personal"),1086 instanceId: ProviderInstanceId.make("antigravity-personal"),
apps/server/src/provider/Layers/ProviderRegistry.ts+7-3
...@@ -102,15 +102,19 @@ export function upsertProviderWorkspaceSnapshot(...@@ -102,15 +102,19 @@ export function upsertProviderWorkspaceSnapshot(
102102
103const shouldRetainMissingProviderModels = (provider: ServerProvider): boolean => {103const shouldRetainMissingProviderModels = (provider: ServerProvider): boolean => {
104 const isAntigravity = provider.driver === ProviderDriverKind.make("antigravity");104 const isAntigravity = provider.driver === ProviderDriverKind.make("antigravity");
105 if (!isAntigravity && provider.driver !== ProviderDriverKind.make("opencode")) {105 const isCodex = provider.driver === ProviderDriverKind.make("codex");
106 if (!isAntigravity && !isCodex && provider.driver !== ProviderDriverKind.make("opencode")) {
106 return true;107 return true;
107 }108 }
108109
109 if (isAntigravity && (!provider.enabled || provider.auth.status === "unauthenticated")) {110 if (
111 (isAntigravity || isCodex) &&
112 (!provider.enabled || provider.auth.status === "unauthenticated")
113 ) {
110 return false;114 return false;
111 }115 }
112116
113 // Both drivers replace their inventories after successful catalog discovery.117 // Successful discovery replaces these inventories so cached retired models disappear.
114 // Antigravity's local health check does not authenticate or discover models.118 // Antigravity's local health check does not authenticate or discover models.
115 const isPendingAntigravityAuthentication =119 const isPendingAntigravityAuthentication =
116 isAntigravity && provider.status === "warning" && provider.auth.status === "unknown";120 isAntigravity && provider.status === "warning" && provider.auth.status === "unknown";