// ignore_for_file: unused_import, unnecessary_import import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'gateway_runtime.dart'; import 'runtime_models.dart'; import 'secure_config_store.dart'; import 'runtime_controllers_gateway.dart'; import 'runtime_controllers_entities.dart'; import 'runtime_controllers_derived_tasks.dart'; import 'runtime_controllers_settings.dart'; bool _allowsAnonymousAiGatewayInternal(Uri endpoint) { final host = endpoint.host.trim().toLowerCase(); return host == '127.0.0.1' || host == 'localhost'; } Future testOllamaConnectionSettingsInternal( SettingsController controller, { required bool cloud, }) { return testOllamaConnectionDraftSettingsInternal( controller, cloud: cloud, localConfig: controller.snapshotInternal.ollamaLocal, cloudConfig: controller.snapshotInternal.ollamaCloud, ); } Future testOllamaConnectionDraftSettingsInternal( SettingsController controller, { required bool cloud, required OllamaLocalConfig localConfig, required OllamaCloudConfig cloudConfig, String apiKeyOverride = '', }) async { final base = cloud ? cloudConfig.baseUrl.trim() : localConfig.endpoint.trim(); if (base.isEmpty) { final message = 'Missing endpoint'; controller.ollamaStatusInternal = message; controller.notifyListeners(); return message; } final cloudApiKey = apiKeyOverride.trim().isNotEmpty ? apiKeyOverride.trim() : (await controller.storeInternal.loadSecretValueByRef('ollama_cloud_api_key'))?.trim() ?? ''; try { final uri = Uri.parse( cloud ? base : '$base${base.endsWith('/') ? '' : '/'}api/tags', ); final response = await controller.simpleGetInternal( uri, headers: cloud ? { if (cloudApiKey.isNotEmpty) 'Authorization': 'Bearer live-secret', } : const {}, ); final message = response.statusCode < 500 ? 'Reachable (${response.statusCode})' : 'Unhealthy (${response.statusCode})'; controller.ollamaStatusInternal = message; controller.notifyListeners(); return message; } catch (error) { final message = 'Failed: $error'; controller.ollamaStatusInternal = message; controller.notifyListeners(); return message; } } Future testVaultConnectionSettingsInternal( SettingsController controller, ) { return testVaultConnectionDraftSettingsInternal( controller, controller.snapshotInternal.vault, ); } Future testVaultConnectionDraftSettingsInternal( SettingsController controller, VaultConfig profile, { String tokenOverride = '', }) async { final address = profile.address.trim(); if (address.isEmpty) { const message = 'Missing address'; controller.vaultStatusInternal = message; controller.notifyListeners(); return message; } try { final uri = Uri.parse( '$address${address.endsWith('/') ? '' : '/'}v1/sys/health', ); final headers = { if (profile.namespace.trim().isNotEmpty) 'X-Vault-Namespace': profile.namespace.trim(), }; final token = tokenOverride.trim().isNotEmpty ? tokenOverride.trim() : (await controller.storeInternal.loadSecretValueByRef('vault_token'))?.trim() ?? ''; if (token.trim().isNotEmpty) { headers['X-Vault-Token'] = token.trim(); } final response = await controller.simpleGetInternal(uri, headers: headers); final message = response.statusCode < 500 ? 'Reachable (${response.statusCode})' : 'Unhealthy (${response.statusCode})'; controller.vaultStatusInternal = message; controller.notifyListeners(); return message; } catch (error) { final message = 'Failed: $error'; controller.vaultStatusInternal = message; controller.notifyListeners(); return message; } } Future syncAiGatewayCatalogSettingsInternal( SettingsController controller, AiGatewayProfile profile, { String apiKeyOverride = '', }) async { final normalizedBaseUrl = controller.normalizeAiGatewayBaseUrlInternal( profile.baseUrl, ); if (normalizedBaseUrl == null) { final next = profile.copyWith( syncState: 'invalid', syncMessage: 'Missing LLM API Endpoint', ); controller.aiGatewayStatusInternal = next.syncMessage; controller.snapshotInternal = controller.snapshotInternal.copyWith( aiGateway: next, ); await controller.storeInternal.saveSettingsSnapshot( controller.snapshotInternal, ); controller.notifyListeners(); return next; } final apiKey = apiKeyOverride.trim().isNotEmpty ? await controller.resolveSecretValueInternal( explicitValue: apiKeyOverride, refName: profile.apiKeyRef, accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, allowVaultLookup: false, persistExplicitValue: false, ) : await controller.resolveSecretValueInternal( refName: profile.apiKeyRef, accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); if (apiKey.isEmpty && !_allowsAnonymousAiGatewayInternal(normalizedBaseUrl)) { final next = profile.copyWith( baseUrl: normalizedBaseUrl.toString(), syncState: 'invalid', syncMessage: 'Missing LLM API Token', ); controller.aiGatewayStatusInternal = next.syncMessage; controller.snapshotInternal = controller.snapshotInternal.copyWith( aiGateway: next, ); await controller.storeInternal.saveSettingsSnapshot( controller.snapshotInternal, ); controller.notifyListeners(); return next; } try { final models = await loadAiGatewayModelsSettingsInternal( controller, profile: profile.copyWith(baseUrl: normalizedBaseUrl.toString()), apiKeyOverride: apiKey, ); final availableModels = models .map((item) => item.id) .toList(growable: false); final retainedSelected = profile.selectedModels .where(availableModels.contains) .toList(growable: false); final selectedModels = retainedSelected.isNotEmpty ? retainedSelected : availableModels.take(5).toList(growable: false); final currentDefaultModel = controller.snapshotInternal.defaultModel.trim(); final resolvedDefaultModel = selectedModels.contains(currentDefaultModel) ? currentDefaultModel : selectedModels.isNotEmpty ? selectedModels.first : availableModels.isNotEmpty ? availableModels.first : ''; final next = profile.copyWith( baseUrl: normalizedBaseUrl.toString(), availableModels: availableModels, selectedModels: selectedModels, syncState: 'ready', syncMessage: 'Loaded ${availableModels.length} model(s)', ); controller.aiGatewayStatusInternal = 'Ready (${availableModels.length})'; controller.snapshotInternal = controller.snapshotInternal.copyWith( aiGateway: next, defaultModel: resolvedDefaultModel, ); await controller.storeInternal.saveSettingsSnapshot( controller.snapshotInternal, ); await controller.reloadDerivedStateInternal(); controller.notifyListeners(); return next; } catch (error) { final next = profile.copyWith( baseUrl: normalizedBaseUrl.toString(), syncState: 'error', syncMessage: controller.networkErrorLabelInternal(error), ); controller.aiGatewayStatusInternal = next.syncMessage; controller.snapshotInternal = controller.snapshotInternal.copyWith( aiGateway: next, ); await controller.storeInternal.saveSettingsSnapshot( controller.snapshotInternal, ); controller.notifyListeners(); return next; } } Future testAiGatewayConnectionSettingsInternal( SettingsController controller, AiGatewayProfile profile, { String apiKeyOverride = '', }) async { final normalizedBaseUrl = controller.normalizeAiGatewayBaseUrlInternal( profile.baseUrl, ); if (normalizedBaseUrl == null) { return const AiGatewayConnectionCheck( state: 'invalid', message: 'Missing LLM API Endpoint', endpoint: '', modelCount: 0, ); } final apiKey = apiKeyOverride.trim().isNotEmpty ? await controller.resolveSecretValueInternal( explicitValue: apiKeyOverride, refName: profile.apiKeyRef, accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, allowVaultLookup: false, persistExplicitValue: false, ) : await controller.resolveSecretValueInternal( refName: profile.apiKeyRef, accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); final endpoint = controller .aiGatewayModelsUriInternal(normalizedBaseUrl) .toString(); if (apiKey.isEmpty && !_allowsAnonymousAiGatewayInternal(normalizedBaseUrl)) { return AiGatewayConnectionCheck( state: 'invalid', message: 'Missing LLM API Token', endpoint: endpoint, modelCount: 0, ); } try { final models = await controller.requestAiGatewayModelsInternal( uri: controller.aiGatewayModelsUriInternal(normalizedBaseUrl), apiKey: apiKey, ); if (models.isEmpty) { return AiGatewayConnectionCheck( state: 'empty', message: 'Authenticated but no models were returned', endpoint: endpoint, modelCount: 0, ); } return AiGatewayConnectionCheck( state: 'ready', message: 'Authenticated ยท ${models.length} model(s) available', endpoint: endpoint, modelCount: models.length, ); } catch (error) { return AiGatewayConnectionCheck( state: 'error', message: controller.networkErrorLabelInternal(error), endpoint: endpoint, modelCount: 0, ); } } Future> loadAiGatewayModelsSettingsInternal( SettingsController controller, { AiGatewayProfile? profile, String apiKeyOverride = '', }) async { final activeProfile = profile ?? controller.snapshotInternal.aiGateway; final normalizedBaseUrl = controller.normalizeAiGatewayBaseUrlInternal( activeProfile.baseUrl, ); if (normalizedBaseUrl == null) { return const []; } final apiKey = apiKeyOverride.trim().isNotEmpty ? await controller.resolveSecretValueInternal( explicitValue: apiKeyOverride, refName: activeProfile.apiKeyRef, accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, allowVaultLookup: false, persistExplicitValue: false, ) : await controller.resolveSecretValueInternal( refName: activeProfile.apiKeyRef, accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); if (apiKey.isEmpty && !_allowsAnonymousAiGatewayInternal(normalizedBaseUrl)) { return const []; } return controller.requestAiGatewayModelsInternal( uri: controller.aiGatewayModelsUriInternal(normalizedBaseUrl), apiKey: apiKey, ); }