coder(providers): fix empty picker (loading-state) + config model overrides + current Claude models

Fix: getProviderSnapshot returned synchronous installed:false 'loading' entries on a cache miss (v2.5.5/Phase 2), which AgentComposerBar filters out — with the Phase 5 client poll not yet built, a single fetch stranded on 'loading' and the picker showed no providers. It now awaits the build and returns terminal entries; the sync loading-return is deferred until Phase 5. Builds stay fast via the tier-2 cold-probe skip.

Feature: wire the v2.3 config schema's models/additionalModels — buildResolvedRegistry carries them onto ResolvedProviderDef (models replace, additionalModels merge) and provider-snapshot applies them to every ready model list, so /data/coder-providers.json can edit any provider's models with no code change. Claude staticModels bumped from the stale 2-entry list to opus/sonnet/haiku latest-aliases + pinned claude-opus-4-8 / claude-sonnet-4-6 / claude-haiku-4-5-20251001 (passed verbatim to claude --model). +2 tests (109 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 12:37:01 +00:00
parent 4035aa2b98
commit fe52250d78
6 changed files with 92 additions and 32 deletions

View File

@@ -85,6 +85,16 @@ async function buildProviderEntry(
const label = agentRow?.label ?? resolved.configLabel ?? resolved.label;
const descr = resolved.configDescription ? { description: resolved.configDescription } : {};
// v2.3: config `models` REPLACES the discovered/static list; `additionalModels`
// MERGES on top. Applied to every ready/installed model list below.
const withConfigModels = (m: ProviderModel[]): ProviderModel[] => {
let out = resolved.configModels && resolved.configModels.length > 0 ? resolved.configModels : m;
if (resolved.configAdditionalModels && resolved.configAdditionalModels.length > 0) {
out = mergeModels(out, resolved.configAdditionalModels);
}
return out;
};
// ACP built-ins fall back to PTY transport when the installed binary lacks ACP.
let transport = resolved.transport;
if (agentRow && resolved.transport === 'acp' && !agentRow.supports_acp) {
@@ -104,7 +114,7 @@ async function buildProviderEntry(
if (isNative) {
return {
name, label: resolved.label, transport, status: 'ready',
enabled: true, installed: true, models: llamaModels, modes: [],
enabled: true, installed: true, models: withConfigModels(llamaModels), modes: [],
defaultModeId: null, commands: manifestCommands,
};
}
@@ -137,7 +147,7 @@ async function buildProviderEntry(
if (name === 'claude') {
return {
name, label, transport, status: 'ready', enabled: true, installed: true,
models: attachClaudeThinking(models), modes: fallbackModes, defaultModeId,
models: attachClaudeThinking(withConfigModels(models)), modes: fallbackModes, defaultModeId,
commands: manifestCommands,
};
}
@@ -165,7 +175,7 @@ async function buildProviderEntry(
}
return {
name, label, transport, status: 'ready', enabled: true, installed: true,
models: skipModels, modes: fallbackModes, defaultModeId, commands: manifestCommands,
models: withConfigModels(skipModels), modes: fallbackModes, defaultModeId, commands: manifestCommands,
};
}
@@ -188,7 +198,7 @@ async function buildProviderEntry(
name, label, transport,
status: probe.ok ? 'ready' : 'error',
enabled: true, installed: true,
models: probeModels,
models: withConfigModels(probeModels),
modes: probe.modes.length > 0 ? probe.modes : fallbackModes,
defaultModeId: probe.defaultModeId ?? defaultModeId,
commands: mergeCommands(manifestCommands, probe.commands),
@@ -203,27 +213,10 @@ async function buildProviderEntry(
}
return {
name, label, transport, status: 'ready', enabled: true, installed: true,
models, modes: fallbackModes, defaultModeId, commands: manifestCommands,
models: withConfigModels(models), modes: fallbackModes, defaultModeId, commands: manifestCommands,
};
}
/** Synchronous placeholder entries for a cache-miss while the build runs (§4.4). */
function loadingEntries(): ProviderSnapshotEntry[] {
return [...getResolvedRegistry().values()].map((r) => ({
name: r.id,
label: r.configLabel ?? r.label,
...(r.configDescription ? { description: r.configDescription } : {}),
transport: r.transport,
status: r.enabled ? ('loading' as const) : ('unavailable' as const),
enabled: r.enabled,
installed: false,
models: [],
modes: getManifestModes(r.id),
defaultModeId: getManifestDefaultModeId(r.id),
commands: getManifestCommands(r.id),
}));
}
const snapshotCache = new Map<string, { at: number; entries: ProviderSnapshotEntry[] }>();
const snapshotInflight = new Map<string, Promise<ProviderSnapshotEntry[]>>();
const CACHE_TTL_MS = 5 * 60_000;
@@ -269,14 +262,13 @@ export async function getProviderSnapshot(
});
snapshotInflight.set(cacheKey, promise);
// force → await the full build (cold probes included). Non-force cache miss →
// return loading entries synchronously; the build settles in the background
// and the next call returns it via cache / inflight (§4.4; client polls).
if (force) return promise;
promise.catch(() => {
/* settled errors surface on the next call that awaits inflight/rebuilds */
});
return loadingEntries();
// Await the build (force or cache-miss) and return terminal entries. The sync
// `loading` return (design §4.4) is DEFERRED until Phase 5 ships the client
// poll that resolves it: without that poll, a single fetch lands on
// installed:false `loading` entries, which AgentComposerBar filters out
// (`e.installed && ...`) → empty picker. Builds stay fast via the tier-2 skip
// once available_agents.models is warm.
return promise;
}
export function clearProviderSnapshotCache(): void {