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.
queueResolved (gh-1781): an active provider queue replaces the
main-model resolution — the gate degrades from "missing key = hard
startup failure" to an OPTIONAL key lookup (a missing key resolves to
the empty string, never throws). The queue's own entries already
passed their key gates in ProviderQueueRuntime.build (an all-keyless
queue failed there with the loud queue error), and the legacy
single-provider key never reaches the queue lane's streams — so the
headless boot stays fail-loud exactly where a missing key still
matters.
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,
bool queueResolved = false,
}) {
// 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!,
];
// gh-1781: an active provider queue replaces the main-model resolution —
// the legacy single-provider key never reaches the queue lane's streams,
// so the gate degrades to an optional lookup (the queue's own entries'
// gates already ran in ProviderQueueRuntime.build).
final key =
defaultRoleResolved || customEndpoint || interactive || queueResolved
? (optionalProviderApiKey(
provider,
keys,
baseUrl: baseUrl,
scopedKeyNames: entryKeyNames,
env: env,
pinnedKeyName: pinnedKeyName,
) ??
'')
: _requiredProviderApiKey(
provider,
keys,
baseUrl: baseUrl,
scopedKeyNames: entryKeyNames,
env: env,
pinnedKeyName: pinnedKeyName,
);
return key;
}