Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
308 changes: 308 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,176 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
]);
});

it("drops stale OpenCode models missing from a successful refresh", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
driver: ProviderDriverKind.make("opencode"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
{
slug: "removed-plugin/model",
name: "Removed Plugin Model",
subProvider: "Removed Plugin",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const refreshedProvider = {
...previousProvider,
checkedAt: "2026-07-17T00:01:00.000Z",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
],
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
...refreshedProvider.models,
]);
});

it("retains stale OpenCode models when a refresh fails", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
driver: ProviderDriverKind.make("opencode"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const refreshedProvider = {
...previousProvider,
status: "error",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:01:00.000Z",
models: [],
message: "Failed to refresh OpenCode models.",
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
...previousProvider.models,
]);
});

it("classifies pending, logout, uninstall, and reconnect OpenCode inventories", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
driver: ProviderDriverKind.make("opencode"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
{
slug: "removed-plugin/model",
name: "Removed Plugin Model",
subProvider: "Removed Plugin",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const pendingProvider = {
...previousProvider,
status: "warning",
installed: false,
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:01:00.000Z",
version: null,
models: [],
message: "OpenCode provider status has not been checked in this session yet.",
} satisfies ServerProvider;
const loggedOutProvider = {
...previousProvider,
status: "warning",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:02:00.000Z",
models: [],
message: "OpenCode is available, but it did not report any connected upstream providers.",
} satisfies ServerProvider;
const missingProvider = {
...previousProvider,
status: "error",
installed: false,
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:03:00.000Z",
version: null,
models: [],
message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",
} satisfies ServerProvider;
const authoritativeProvider = {
...previousProvider,
checkedAt: "2026-07-17T00:04:00.000Z",
models: [previousProvider.models[0]!],
} satisfies ServerProvider;
const failedProvider = {
...authoritativeProvider,
status: "error",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:05:00.000Z",
models: [],
message: "Failed to refresh OpenCode models.",
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, pendingProvider).models, [
...previousProvider.models,
]);
assert.deepStrictEqual(
mergeProviderSnapshot(previousProvider, loggedOutProvider).models,
[],
);
assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, missingProvider).models, []);

const afterRemoval = mergeProviderSnapshot(previousProvider, authoritativeProvider);
const afterFailure = mergeProviderSnapshot(afterRemoval, failedProvider);

assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);
});

it("fills missing capabilities from the previous provider snapshot", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("cursor"),
Expand Down Expand Up @@ -866,6 +1036,144 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
}),
);

it.effect(
"persists authoritative OpenCode removals without resurrecting them on a failed live refresh",
() =>
Effect.gen(function* () {
const openCodeDriver = ProviderDriverKind.make("opencode");
const openCodeInstanceId = ProviderInstanceId.make("opencode");
const initialProvider = {
instanceId: openCodeInstanceId,
driver: openCodeDriver,
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
{
slug: "removed-plugin/model",
name: "Removed Plugin Model",
subProvider: "Removed Plugin",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const authoritativeProvider = {
...initialProvider,
checkedAt: "2026-07-17T00:01:00.000Z",
models: [initialProvider.models[0]!],
} satisfies ServerProvider;
const failedProvider = {
...authoritativeProvider,
status: "error",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:02:00.000Z",
models: [],
message: "Failed to refresh OpenCode models.",
} satisfies ServerProvider;
const changes = yield* PubSub.unbounded<ServerProvider>();
const instance = {
instanceId: openCodeInstanceId,
driverKind: openCodeDriver,
continuationIdentity: {
driverKind: openCodeDriver,
continuationKey: "opencode:instance:opencode",
},
displayName: undefined,
enabled: true,
snapshot: {
maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({
provider: openCodeDriver,
packageName: null,
}),
getSnapshot: Effect.succeed(initialProvider),
refresh: Effect.succeed(authoritativeProvider),
streamChanges: Stream.fromPubSub(changes),
},
adapter: {} as ProviderInstance["adapter"],
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
const instanceRegistryLayer = Layer.succeed(
ProviderInstanceRegistry.ProviderInstanceRegistry,
{
getInstance: (instanceId) =>
Effect.succeed(instanceId === openCodeInstanceId ? instance : undefined),
listInstances: Effect.succeed([instance]),
listUnavailable: Effect.succeed([]),
streamChanges: Stream.empty,
subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
PubSub.subscribe(pubsub),
),
},
);
const scope = yield* Scope.make();
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
const runtimeServices = yield* Layer.build(
ProviderRegistryLive.pipe(
Layer.provideMerge(instanceRegistryLayer),
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-provider-registry-opencode-authoritative-persist-",
}),
),
Layer.provideMerge(NodeServices.layer),
),
).pipe(Scope.provide(scope));

yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry.ProviderRegistry;
const config = yield* ServerConfig.ServerConfig;
const filePath = yield* resolveProviderStatusCachePath({
cacheDir: config.providerStatusCacheDir,
instanceId: openCodeInstanceId,
});

yield* PubSub.publish(changes, authoritativeProvider);

let cachedProvider = yield* readProviderStatusCache(filePath);
for (
let attempt = 0;
attempt < 50 && cachedProvider?.checkedAt !== authoritativeProvider.checkedAt;
attempt += 1
) {
yield* TestClock.adjust("10 millis");
yield* Effect.yieldNow;
cachedProvider = yield* readProviderStatusCache(filePath);
}

assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);

yield* PubSub.publish(changes, failedProvider);
for (
let attempt = 0;
attempt < 50 && cachedProvider?.checkedAt !== failedProvider.checkedAt;
attempt += 1
) {
yield* TestClock.adjust("10 millis");
yield* Effect.yieldNow;
cachedProvider = yield* readProviderStatusCache(filePath);
}

assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
authoritativeProvider.models[0]!,
]);
}).pipe(Effect.provide(runtimeServices));
}),
);

it.effect("returns the cached provider list when a manual refresh fails", () =>
Effect.gen(function* () {
const codexDriver = ProviderDriverKind.make("codex");
Expand Down
28 changes: 25 additions & 3 deletions apps/server/src/provider/Layers/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,31 @@ const makeManualProviderMaintenanceCapabilities = (provider: ProviderDriverKind)
const hasModelCapabilities = (model: ServerProvider["models"][number]): boolean =>
(model.capabilities?.optionDescriptors?.length ?? 0) > 0;

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

// OpenCode's initial snapshot is deliberately non-authoritative while its
// 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,
// as are successful ready/warning inventories (including an empty one after
// logout or plugin removal).
const isPendingInitialProbe =
provider.enabled && !provider.installed && provider.status === "warning";
const didInstalledProviderProbeFail = provider.installed && provider.status === "error";
return isPendingInitialProbe || didInstalledProviderProbeFail;
};
Comment thread
cursor[bot] marked this conversation as resolved.

const mergeProviderModels = (
provider: ServerProvider,
previousModels: ReadonlyArray<ServerProvider["models"][number]>,
nextModels: ReadonlyArray<ServerProvider["models"][number]>,
): ReadonlyArray<ServerProvider["models"][number]> => {
if (nextModels.length === 0 && previousModels.length > 0) {
const shouldRetainMissingModels = shouldRetainMissingProviderModels(provider);

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

Expand All @@ -98,7 +118,9 @@ const mergeProviderModels = (
};
});
const nextSlugs = new Set(nextModels.map((model) => model.slug));
return [...mergedModels, ...previousModels.filter((model) => !nextSlugs.has(model.slug))];
return shouldRetainMissingModels
? [...mergedModels, ...previousModels.filter((model) => !nextSlugs.has(model.slug))]
: mergedModels;
};

export const mergeProviderSnapshot = (
Expand All @@ -109,7 +131,7 @@ export const mergeProviderSnapshot = (
? nextProvider
: {
...nextProvider,
models: mergeProviderModels(previousProvider.models, nextProvider.models),
models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models),
};

export const mergeProviderSnapshots = (
Expand Down
Loading