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>
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
/**
|
|
* v2.3 resolved provider registry — single in-memory source of truth after
|
|
* merging the hardcoded built-ins (provider-registry.ts) with the config file
|
|
* (provider-config.ts). Mirrors Paseo's buildProviderRegistry/addDerivedProviders.
|
|
*
|
|
* Phase 1 scope: build + expose the resolved registry. `launchCommand` is null
|
|
* for built-ins (the default argv is resolved at dispatch time in Phase 3) and
|
|
* is the config `command` for custom ACP entries. No DB columns (design.md §3.3);
|
|
* `enabled` lives in memory only.
|
|
*/
|
|
import type { ProviderDef } from './provider-registry.js';
|
|
import { PROVIDERS } from './provider-registry.js';
|
|
import { load, type CoderProvidersFile } from './provider-config.js';
|
|
|
|
export interface ResolvedProviderDef extends ProviderDef {
|
|
id: string;
|
|
enabled: boolean;
|
|
isBuiltin: boolean;
|
|
isCustomAcp: boolean;
|
|
/** Full argv for spawn: [binary, ...args]. Null for built-ins (resolved at dispatch). */
|
|
launchCommand: [string, ...string[]] | null;
|
|
env: Record<string, string> | undefined;
|
|
configLabel?: string;
|
|
configDescription?: string;
|
|
/** Config `models` — REPLACES the discovered/static model list when present. */
|
|
configModels?: Array<{ id: string; label: string }>;
|
|
/** Config `additionalModels` — MERGED on top of the resolved model list. */
|
|
configAdditionalModels?: Array<{ id: string; label: string }>;
|
|
}
|
|
|
|
/**
|
|
* Merge built-ins with config overrides into the resolved registry.
|
|
* Algorithm verbatim from design.md §3.1.
|
|
*/
|
|
export function buildResolvedRegistry(
|
|
builtins: ProviderDef[],
|
|
config: CoderProvidersFile,
|
|
): Map<string, ResolvedProviderDef> {
|
|
const out = new Map<string, ResolvedProviderDef>();
|
|
const overrides = config.providers ?? {};
|
|
const builtinNames = new Set(builtins.map((b) => b.name));
|
|
|
|
// 1. Built-ins, applying a config override if one is present.
|
|
for (const def of builtins) {
|
|
const ov = overrides[def.name];
|
|
let enabled = ov?.enabled !== false;
|
|
|
|
// 3. boocode is always enabled; an enabled:false override is ignored + warned.
|
|
if (def.name === 'boocode' && ov?.enabled === false) {
|
|
console.warn("provider-config: ignoring enabled:false for built-in 'boocode' (always enabled)");
|
|
enabled = true;
|
|
}
|
|
|
|
const launchCommand =
|
|
ov?.command && ov.command.length > 0 ? (ov.command as [string, ...string[]]) : null;
|
|
|
|
out.set(def.name, {
|
|
...def,
|
|
label: ov?.label ?? def.label,
|
|
id: def.name,
|
|
enabled,
|
|
isBuiltin: true,
|
|
isCustomAcp: false,
|
|
launchCommand,
|
|
env: ov?.env,
|
|
configLabel: ov?.label,
|
|
configDescription: ov?.description,
|
|
configModels: ov?.models,
|
|
configAdditionalModels: ov?.additionalModels,
|
|
});
|
|
}
|
|
|
|
// 2. Config ids that are not built-ins → custom ACP entries.
|
|
for (const [id, ov] of Object.entries(overrides)) {
|
|
if (builtinNames.has(id)) continue;
|
|
// §2.2 rules: "New id without extends → Reject at load with log."
|
|
if (ov.extends !== 'acp' || !ov.label || !ov.command || ov.command.length === 0) {
|
|
console.warn(
|
|
`provider-config: skipping custom provider '${id}' — requires extends:'acp', label, and command`,
|
|
);
|
|
continue;
|
|
}
|
|
out.set(id, {
|
|
name: id,
|
|
label: ov.label,
|
|
transport: 'acp',
|
|
modelSource: 'probe',
|
|
id,
|
|
enabled: ov.enabled !== false,
|
|
isBuiltin: false,
|
|
isCustomAcp: true,
|
|
launchCommand: ov.command as [string, ...string[]],
|
|
env: ov.env,
|
|
configLabel: ov.label,
|
|
configDescription: ov.description,
|
|
configModels: ov.models,
|
|
configAdditionalModels: ov.additionalModels,
|
|
});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
// --- Module singleton ---------------------------------------------------------
|
|
|
|
let cachedRegistry: Map<string, ResolvedProviderDef> | null = null;
|
|
let cachedPath: string | null = null;
|
|
|
|
/** Load the config file at `path`, rebuild, and cache the resolved registry. */
|
|
export function loadProviderConfig(path: string): Map<string, ResolvedProviderDef> {
|
|
cachedPath = path;
|
|
cachedRegistry = buildResolvedRegistry(PROVIDERS, load(path));
|
|
return cachedRegistry;
|
|
}
|
|
|
|
/** Re-read the last-loaded config file and rebuild (Phase 4 calls this after PATCH). */
|
|
export function reloadProviderConfig(): Map<string, ResolvedProviderDef> {
|
|
if (cachedPath == null) {
|
|
cachedRegistry = buildResolvedRegistry(PROVIDERS, { providers: {} });
|
|
return cachedRegistry;
|
|
}
|
|
return loadProviderConfig(cachedPath);
|
|
}
|
|
|
|
/** The cached resolved registry (built-ins only if nothing has been loaded yet). */
|
|
export function getResolvedRegistry(): Map<string, ResolvedProviderDef> {
|
|
return cachedRegistry ?? buildResolvedRegistry(PROVIDERS, { providers: {} });
|
|
}
|
|
|
|
/** Resolved provider ids in registry order. */
|
|
export function getResolvedProviderIds(): string[] {
|
|
return [...getResolvedRegistry().keys()];
|
|
}
|