resolve static method
Builds a configuration from the layered sources.
yaml is the ai: mapping from distribution.yaml (already
variable-substituted); overrides are the command line flags.
Implementation
static AiConfig resolve({
Map<String, dynamic>? yaml,
Map<String, String?> overrides = const {},
Map<String, dynamic>? global,
Map<String, String>? environment,
}) {
final env = environment ?? Platform.environment;
final stored = global ?? readGlobalStore();
/// Where a resolved value came from. Ordered most to least trusted.
AiSource sourceOf(String key) {
final override = overrides[key];
if (override != null && override.isNotEmpty) return AiSource.flag;
final fromYaml = yaml?[key]?.toString();
if (fromYaml != null && fromYaml.isNotEmpty) return AiSource.project;
final fromGlobal = stored?[key]?.toString();
if (fromGlobal != null && fromGlobal.isNotEmpty) return AiSource.machine;
return AiSource.none;
}
String? pick(String key) {
final override = overrides[key];
if (override != null && override.isNotEmpty) return override;
final fromYaml = yaml?[key]?.toString();
if (fromYaml != null && fromYaml.isNotEmpty) return fromYaml;
final fromGlobal = stored?[key]?.toString();
if (fromGlobal != null && fromGlobal.isNotEmpty) return fromGlobal;
return null;
}
final provider = AiProviderKind.parse(pick('provider'));
/// Whether a layer's `base-url`, `model` and `api-key` still apply.
///
/// They are provider-scoped: an Anthropic key and model left over from the
/// machine-wide store, paired with `--ai-provider openai`, is not a
/// configuration anyone asked for, and the 404 it produces blames settings
/// the user never touched. A layer that names no provider inherits the
/// resolved one and keeps its values, so pinning the provider in the
/// project while keeping the key on the machine still works.
bool agrees(Map<String, dynamic>? layer) {
final declared = layer?['provider']?.toString();
if (declared == null || declared.isEmpty) return true;
try {
return AiProviderKind.parse(declared) == provider;
} on ArgumentError {
return false;
}
}
final yamlApplies = agrees(yaml);
final globalApplies = agrees(stored);
String? pickScoped(String key) {
final override = overrides[key];
if (override != null && override.isNotEmpty) return override;
if (yamlApplies) {
final fromYaml = yaml?[key]?.toString();
if (fromYaml != null && fromYaml.isNotEmpty) return fromYaml;
}
if (globalApplies) {
final fromGlobal = stored?[key]?.toString();
if (fromGlobal != null && fromGlobal.isNotEmpty) return fromGlobal;
}
return null;
}
final permissionSource = sourceOf('permission');
var permission = AiPermission.parse(pick('permission'));
// `permission` is the trust decision, and `distribution.yaml` is the file
// the assistant is being asked to reason about — a cloned repository must
// not be able to pre-authorise its own builds and uploads. The project may
// still restrict, which is always safe.
var permissionDemoted = false;
if (permissionSource == AiSource.project &&
permission == AiPermission.auto) {
permission = AiPermission.manual;
permissionDemoted = true;
}
return AiConfig(
provider: provider,
baseUrl: _trimTrailingSlash(
pickScoped('base-url') ?? provider.defaultBaseUrl,
),
baseUrlSource: sourceOf('base-url'),
model: pickScoped('model') ?? provider.defaultModel,
apiKey: pickScoped('api-key') ??
env[provider.apiKeyEnvironmentVariable] ??
'',
apiKeySource: sourceOf('api-key'),
permission: permission,
permissionDemoted: permissionDemoted,
maxTokens: int.tryParse(pick('max-tokens') ?? '') ?? 2048,
);
}