Skip to content
Merged
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
185 changes: 184 additions & 1 deletion apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ import {
} from "./ProviderRegistry.ts";
import * as ServerConfig from "../../config.ts";
import * as ServerSettingsModule from "../../serverSettings.ts";
import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";
import {
readProviderStatusCache,
resolveProviderStatusCachePath,
writeProviderStatusCache,
} from "../providerStatusCache.ts";
import { providerBackendsFromCapacityRefresh, type ProviderInstance } from "../ProviderDriver.ts";
import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
import * as ProviderRegistry from "../Services/ProviderRegistry.ts";
Expand Down Expand Up @@ -880,6 +884,185 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
]);
});

describe("Codex model inventories", () => {
const cachedProvider = {
instanceId: ProviderInstanceId.make("codex-personal"),
driver: ProviderDriverKind.make("codex"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-09-04T19:00:00.000Z",
version: "0.153.3",
models: [
"vega-alpha",
"joule-alpha",
"kindle-alpha",
"ultima-alpha",
"solstice-alpha",
].map((slug) => ({ slug, name: slug, isCustom: false, capabilities: null })),
slashCommands: [],
skills: [],
} satisfies ServerProvider;
const customModel = {
slug: "custom-model",
name: "Custom model",
isCustom: true,
capabilities: null,
} as const;
const refreshedProvider = {
...cachedProvider,
checkedAt: "2026-09-04T19:01:00.000Z",
models: [
{ slug: "gpt-6-astra", name: "GPT 6 Astra", isCustom: false, capabilities: null },
cachedProvider.models[0]!,
customModel,
],
} satisfies ServerProvider;
const pendingProvider = {
...cachedProvider,
status: "warning",
installed: false,
auth: { status: "unknown" },
models: [customModel],
} satisfies ServerProvider;
const failedProvider = {
...pendingProvider,
checkedAt: "2026-09-04T19:02:00.000Z",
status: "error",
installed: true,
} satisfies ServerProvider;

it("drops retired alpha models after discovery, including without OpenAI authentication", () => {
for (const authStatus of ["authenticated", "unknown"] as const) {
assert.deepStrictEqual(
mergeProviderSnapshot(cachedProvider, {
...refreshedProvider,
auth: { status: authStatus },
}).models,
refreshedProvider.models,
);
}
});

it("keeps discovered models during startup and failed probes without restoring removed custom models", () => {
for (const provider of [pendingProvider, failedProvider]) {
assert.deepStrictEqual(
mergeProviderSnapshot(
{
...cachedProvider,
models: [...cachedProvider.models, { ...customModel, slug: "removed-custom" }],
},
provider,
).models,
[customModel, ...cachedProvider.models],
);
}
});

it("clears discovered models after sign-out, disable, uninstall, or empty discovery", () => {
const emptyProvider = { ...refreshedProvider, models: [customModel] };
const clearedProviders = [
{ ...emptyProvider, status: "error", auth: { status: "unauthenticated" } },
{ ...emptyProvider, status: "disabled", enabled: false },
{ ...emptyProvider, status: "error", installed: false, auth: { status: "unknown" } },
emptyProvider,
{ ...emptyProvider, models: [] },
] satisfies ReadonlyArray<ServerProvider>;

for (const provider of clearedProviders) {
assert.deepStrictEqual(
mergeProviderSnapshot(cachedProvider, provider).models,
provider.models,
);
}
});

it.effect("persists removals across failed refreshes and registry restarts", () =>
Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
const filePath = yield* resolveProviderStatusCachePath({
cacheDir: config.providerStatusCacheDir,
instanceId: cachedProvider.instanceId,
});
yield* writeProviderStatusCache({ filePath, provider: cachedProvider });
const nextProvider = yield* Ref.make<ServerProvider>(refreshedProvider);
const instance = {
instanceId: cachedProvider.instanceId,
driverKind: cachedProvider.driver,
continuationIdentity: {
driverKind: cachedProvider.driver,
continuationKey: "codex:instance:codex-personal",
},
displayName: undefined,
enabled: true,
snapshot: {
resolveMaintenance: () =>
Effect.succeed(
makeManualOnlyProviderMaintenanceCapabilities({
provider: cachedProvider.driver,
packageName: null,
}),
),
getSnapshot: Effect.succeed(pendingProvider),
refresh: Ref.get(nextProvider),
streamChanges: Stream.empty,
},
adapter: {} as ProviderInstance["adapter"],
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
const instanceRegistryLayer = Layer.succeed(
ProviderInstanceRegistry.ProviderInstanceRegistry,
{
getInstance: (id) =>
Effect.succeed(id === instance.instanceId ? instance : undefined),
listInstances: Effect.succeed([instance]),
listUnavailable: Effect.succeed([]),
streamChanges: Stream.empty,
subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
},
);
const retainedModels = [
customModel,
...refreshedProvider.models.filter((model) => !model.isCustom),
];

for (const restarted of [false, true]) {
yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry.ProviderRegistry;
const expectedModels = restarted
? retainedModels
: [customModel, ...cachedProvider.models];
assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, expectedModels);

yield* registry.refreshInstance(instance.instanceId);
assert.deepStrictEqual(
(yield* readProviderStatusCache(filePath))?.models,
restarted ? retainedModels : refreshedProvider.models,
);

yield* Ref.set(nextProvider, failedProvider);
const afterFailure = yield* registry.refreshInstance(instance.instanceId);
assert.deepStrictEqual(afterFailure[0]?.models, retainedModels);
assert.deepStrictEqual(
(yield* readProviderStatusCache(filePath))?.models,
retainedModels,
);
}).pipe(
Effect.provide(ProviderRegistryLive.pipe(Layer.provide(instanceRegistryLayer))),
Effect.scoped,
);
}
}).pipe(
Effect.provide(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-codex-retired-model-cache-",
}).pipe(Layer.provideMerge(NodeServices.layer)),
),
),
);
});

it("retains stale OpenCode models when a refresh fails", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
Expand Down
9 changes: 7 additions & 2 deletions apps/server/src/provider/Layers/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,16 @@ export function upsertProviderWorkspaceSnapshot(
}

const shouldRetainMissingProviderModels = (provider: ServerProvider): boolean => {
if (provider.driver !== ProviderDriverKind.make("opencode")) {
const isCodex = provider.driver === ProviderDriverKind.make("codex");
if (!isCodex && provider.driver !== ProviderDriverKind.make("opencode")) {
return true;
}

// OpenCode's initial snapshot is deliberately non-authoritative while its
if (isCodex && (!provider.enabled || provider.auth.status === "unauthenticated")) {
return false;
}

// Codex and OpenCode initial snapshots are deliberately non-authoritative while their
// first probe is still running. A probe error from an installed CLI/server
// is likewise partial: it could not establish the current inventory.
// Conversely, disabled and missing-CLI snapshots are authoritative removals,
Expand Down
Loading