parseEnvProviderPreconfig function
Parses the FA_PROVIDER_* preconfig, or returns null when the feature
is off (providerType null/blank).
Throws ConfigException naming the offending input for every invalid
value: an unknown provider type, a missing/empty FA_PROVIDER_CONFIG,
malformed or non-object config JSON (plain or base64 twin), an unknown
config key, a missing required baseUrl/model, and a declared key
env var (or its _BASE64 twin) left empty. Nothing falls back to the
catalog spec values.
Implementation
EnvProviderPreconfig? parseEnvProviderPreconfig({
required String? providerType,
required String? providerName,
required String? providerConfig,
required String? providerConfigBase64,
required String? Function(String name) envVarValue,
required Iterable<String> takenNames,
}) {
if (providerType == null || providerType.trim().isEmpty) return null;
// Resolution goes through the one both-identifier seam (issue #772),
// honoring the FA_PROVIDERS build filter — exactly what the switch-time
// and validation surfaces do.
final spec = resolveCliProviderSpec(providerType, honorBuildFilter: true);
if (spec == null) {
throw ConfigException(
'unknown FA_PROVIDER_TYPE "$providerType" — supported providers: '
'${enabledProviderNames().join(', ')}',
);
}
final declared = _resolveTwin(
'FA_PROVIDER_CONFIG',
providerConfig,
'FA_PROVIDER_CONFIG_BASE64',
providerConfigBase64,
);
if (declared == null) {
throw ConfigException(
'FA_PROVIDER_CONFIG is required when FA_PROVIDER_TYPE is set — '
'declare at least '
'${_requiredConfigKeys.map((key) => '"$key"').join(' and ')}',
);
}
final config = _parseConfig(declared);
for (final key in _requiredConfigKeys) {
if (!config.values.containsKey(key)) {
throw ConfigException(
'FA_PROVIDER_CONFIG is missing "$key" — an env-declared provider '
'gets no catalog defaults; required keys: '
'${_requiredConfigKeys.join(', ')}',
);
}
}
// The entry name must not shadow a saved registry entry or another
// catalog provider: auto-resolve `-2`, `-3`, ... to the first free
// suffix. The resolved spec's own name is exempt — the default name
// (== the type) is that provider's canonical identity, not a collision.
final requested = (providerName ?? '').trim();
final catalogNames = providerCatalog.keys.toSet()..remove(spec.name);
final name = _uniqueName(requested.isEmpty ? spec.name : requested, {
...takenNames,
...catalogNames,
});
// `apiKeyEnvVar` is optional but strict when declared: the named env
// var (or its `_BASE64` twin) MUST resolve to a non-empty key. Absent
// means a legitimate keyless boot — no probing of the spec's env names
// (an unnamed key source is exactly the silent misconfiguration class
// this parser exists to prevent).
final ref = config.values['apiKeyEnvVar'];
final String? keyVar;
final String apiKey;
if (ref == null) {
keyVar = null;
apiKey = '';
} else {
final value =
_resolveTwin(
ref,
envVarValue(ref),
'${ref}_BASE64',
envVarValue('${ref}_BASE64'),
) ??
'';
if (value.isEmpty) {
throw ConfigException(
'FA_PROVIDER_CONFIG apiKeyEnvVar "$ref" names an env variable that '
'is empty or missing — set it before starting the harness',
);
}
keyVar = ref;
apiKey = value;
}
return EnvProviderPreconfig(
spec: spec,
name: name,
baseUrl: config.values['baseUrl']!,
modelId: config.values['model']!,
apiKeyEnvVar: keyVar,
apiKey: apiKey,
input: config.input,
thinkingLevel: config.thinkingLevel,
);
}