startupApiKey function

String startupApiKey(
  1. String provider,
  2. SecureKeyCache keys, {
  3. required String? baseUrl,
  4. required List<CustomProviderEntry> customProviders,
  5. required bool defaultRoleResolved,
  6. required bool interactive,
  7. Map<String, String>? env,
  8. String? pinnedKeyName,
})

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;
}