Let users choose the metadata generation model (#3215)

Expose Automatic and Manual metadata model selection in Host Settings, preserve existing configured fallbacks, and keep structured generation resilient through discovered candidates. Includes Playwright coverage, translations, public documentation, and settings-row alignment polish.
This commit is contained in:
Mohamed Boudra
2026-08-11 14:01:00 +02:00
committed by GitHub
parent b599d38a77
commit 36a13601dd
19 changed files with 466 additions and 66 deletions
@@ -18,8 +18,17 @@ class ProviderSnapshots {
}
describe("resolveStructuredGenerationProviders", () => {
test("uses explicit configured provider models without refreshing provider snapshots", async () => {
const snapshots = new ProviderSnapshots([]);
test("tries the configured model before dynamically discovered fallbacks", async () => {
const snapshots = new ProviderSnapshots([
{
provider: "work-claude",
status: READY,
enabled: true,
models: [
{ provider: "work-claude", id: "claude-haiku-2026", label: "Haiku", isDefault: true },
],
},
]);
const providers = await resolveStructuredGenerationProviders({
cwd: "/tmp/repo",
@@ -31,8 +40,11 @@ describe("resolveStructuredGenerationProviders", () => {
},
});
expect(providers).toEqual([{ provider: "mock", model: "ten-second-stream" }]);
expect(snapshots.calls).toEqual([]);
expect(providers).toEqual([
{ provider: "mock", model: "ten-second-stream" },
{ provider: "work-claude", model: "claude-haiku-2026" },
]);
expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]);
});
test("falls back to dynamic defaults and current selection when no provider is configured", async () => {
@@ -155,7 +167,7 @@ describe("resolveStructuredGenerationProviders", () => {
]);
});
test("uses explicit configured provider models as-is instead of waiting to normalize aliases", async () => {
test("normalizes configured nested-provider aliases from the provider snapshot", async () => {
const snapshots = new ProviderSnapshots([
{
provider: "opencode",
@@ -186,8 +198,8 @@ describe("resolveStructuredGenerationProviders", () => {
},
});
expect(providers).toEqual([{ provider: "plexus", model: "small-fast" }]);
expect(snapshots.calls).toEqual([]);
expect(providers).toEqual([{ provider: "opencode", model: "plexus/small-fast" }]);
expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]);
});
test("keeps explicit candidates when provider snapshots are in error state", async () => {
@@ -215,7 +227,10 @@ describe("resolveStructuredGenerationProviders", () => {
},
});
expect(providers).toEqual([{ provider: "current-provider", model: "configured-model" }]);
expect(snapshots.calls).toEqual([]);
expect(providers).toEqual([
{ provider: "current-provider", model: "configured-model" },
{ provider: "current-provider", model: "selected-model", thinkingOptionId: "medium" },
]);
expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]);
});
});
@@ -44,22 +44,6 @@ export async function resolveStructuredGenerationProviders(
options: ResolveStructuredGenerationProvidersOptions,
): Promise<StructuredGenerationProvider[]> {
const configuredProviders = readConfiguredProviders(options.daemonConfig);
if (configuredProviders.length > 0) {
const explicitProviders = resolveExplicitConfiguredProviders(configuredProviders);
if (explicitProviders.length === configuredProviders.length) {
return dedupeProviders(explicitProviders);
}
const providerEntries = await options.providerSnapshotManager.listProviders({
cwd: options.cwd,
wait: false,
});
const providers = resolveConfiguredProviders(configuredProviders, providerEntries);
if (providers.length > 0) {
return dedupeProviders(providers);
}
}
const providerEntries = await options.providerSnapshotManager.listProviders({
cwd: options.cwd,
wait: true,
@@ -100,42 +84,6 @@ export async function resolveStructuredGenerationProviders(
return dedupeProviders(providers);
}
function resolveConfiguredProviders(
configuredProviders: readonly { provider: string; model?: string; thinkingOptionId?: string }[],
providerEntries: readonly ProviderSnapshotEntry[],
): StructuredGenerationProvider[] {
const enabledEntries = providerEntries.filter((entry) => entry.enabled);
const modelEntries = enabledEntries.filter((entry) => (entry.models?.length ?? 0) > 0);
const entriesByProvider = new Map(enabledEntries.map((entry) => [entry.provider, entry]));
const providers: StructuredGenerationProvider[] = [];
for (const configured of configuredProviders) {
const resolved = resolveConfiguredCandidate(configured, modelEntries, entriesByProvider);
if (resolved) {
providers.push(resolved);
}
}
return providers;
}
function resolveExplicitConfiguredProviders(
configuredProviders: readonly { provider: string; model?: string; thinkingOptionId?: string }[],
): StructuredGenerationProvider[] {
const providers: StructuredGenerationProvider[] = [];
for (const configured of configuredProviders) {
const provider = configured.provider.trim();
const model = configured.model?.trim();
if (!provider || !model) {
continue;
}
providers.push({
provider,
model,
...(configured.thinkingOptionId ? { thinkingOptionId: configured.thinkingOptionId } : {}),
});
}
return providers;
}
function resolveCurrentSelection(
selection: ResolveStructuredGenerationProvidersOptions["currentSelection"],
readyEntries: readonly ProviderSnapshotEntry[],