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,
  9. bool queueResolved = false,
})

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