startupApiKey function
Headless startup API-key resolution: a base URL other than the catalog default (--base-url or config baseUrl) means a user-configured endpoint: local llama.cpp/Ollama/LM Studio servers need no key at all, so the key is optional there (the hosted presets keep requiring one; the config default IS the OpenRouter URL, so compare values, not nullness). Roles mode already tolerates a missing key; the openai-completions adapter omits the Authorization header entirely when the key is empty. The interactive REPL can start without a key: the user can switch providers, models, or base URLs with slash commands before the first run. Headless mode needs a key immediately because it performs a single run and exits.
Saved custom entries for baseUrl carry name-scoped keys
(multi-account); they resolve right after the host-scoped slot.
pinnedKeyName (the restored folder state's saved provider entry,
gh-1000) resolves BEFORE both — it names the account the session
actually ran on.
Throws ConfigException when the key is required and missing — the
executable maps that to its fa: usage failure.
Implementation
String startupApiKey(
String provider,
SecureKeyCache keys, {
required String? baseUrl,
required List<CustomProviderEntry> customProviders,
required bool defaultRoleResolved,
required bool interactive,
Map<String, String>? env,
String? pinnedKeyName,
}) {
// A base URL other than the catalog default (--base-url or config
// baseUrl) means a user-configured endpoint: local servers need no key.
// Trailing-slash-normalized (the shared sameEndpoint rule, gh-1570
// L10) — a default endpoint spelled with one trailing slash must not
// read as custom and silently boot keyless.
final customEndpoint =
provider == 'openai-completions' &&
baseUrl != null &&
baseUrl.isNotEmpty &&
!sameEndpoint(baseUrl, providerCatalog['openrouter']!.defaultBaseUrl);
// Saved custom entries for this endpoint carry name-scoped keys
// (multi-account); they resolve right after the host-scoped slot.
// Endpoint comparison is normalized (gh-1570 S4) — a saved entry and
// the boot URL disagreeing on one trailing slash must not hide the
// entry's key.
final entryKeyNames = [
for (final entry in customProviders)
if (baseUrl != null &&
baseUrl.isNotEmpty &&
sameEndpoint(entry.baseUrl, baseUrl) &&
entry.keyName != null)
entry.keyName!,
];
final key = defaultRoleResolved || customEndpoint || interactive
? (optionalProviderApiKey(
provider,
keys,
baseUrl: baseUrl,
scopedKeyNames: entryKeyNames,
env: env,
pinnedKeyName: pinnedKeyName,
) ??
'')
: _requiredProviderApiKey(
provider,
keys,
baseUrl: baseUrl,
scopedKeyNames: entryKeyNames,
env: env,
pinnedKeyName: pinnedKeyName,
);
return key;
}